● Zion Boggan
repos/Agent Time Ledger/internal/clock/clock.go
zionboggan.com ↗
168 lines · go
History for this file →
1
package clock
2
 
3
import (
4
	"fmt"
5
	"math"
6
	"regexp"
7
	"strconv"
8
	"strings"
9
	"time"
10
)
11
 
12
const (
13
	ConfidenceHostClock    = "host_clock"
14
	ConfidenceMonotonic    = "monotonic"
15
	ConfidenceWallFallback = "wall_clock_fallback"
16
	ConfidenceUnknown      = "unknown"
17
)
18
 
19
var durationPart = regexp.MustCompile(`(?i)(\d+(?:\.\d+)?)(ns|us|µs|ms|s|m|h|d)`)
20
 
21
func Now() time.Time {
22
	return time.Now().UTC()
23
}
24
 
25
func FormatRFC3339(t time.Time) string {
26
	return t.UTC().Format(time.RFC3339Nano)
27
}
28
 
29
func FormatRFC3339In(t time.Time, location *time.Location) string {
30
	return t.In(location).Format(time.RFC3339Nano)
31
}
32
 
33
func LoadLocation(name string) (*time.Location, error) {
34
	switch strings.TrimSpace(strings.ToLower(name)) {
35
	case "", "local":
36
		return time.Local, nil
37
	case "utc", "z":
38
		return time.UTC, nil
39
	default:
40
		location, err := time.LoadLocation(name)
41
		if err != nil {
42
			return nil, fmt.Errorf("invalid timezone %q: %w", name, err)
43
		}
44
		return location, nil
45
	}
46
}
47
 
48
func LocationName(location *time.Location) string {
49
	if location == nil {
50
		return "UTC"
51
	}
52
	if location == time.Local {
53
		return "Local"
54
	}
55
	return location.String()
56
}
57
 
58
func UTCOffset(t time.Time) string {
59
	_, offset := t.Zone()
60
	sign := "+"
61
	if offset < 0 {
62
		sign = "-"
63
		offset = -offset
64
	}
65
	hours := offset / 3600
66
	minutes := (offset % 3600) / 60
67
	return fmt.Sprintf("%s%02d:%02d", sign, hours, minutes)
68
}
69
 
70
func ParseRFC3339(value string) (time.Time, error) {
71
	t, err := time.Parse(time.RFC3339Nano, value)
72
	if err != nil {
73
		return time.Time{}, err
74
	}
75
	return t.UTC(), nil
76
}
77
 
78
func ParseDuration(value string) (time.Duration, error) {
79
	value = strings.TrimSpace(value)
80
	if value == "" {
81
		return 0, fmt.Errorf("duration is required")
82
	}
83
 
84
	matches := durationPart.FindAllStringSubmatchIndex(value, -1)
85
	if len(matches) == 0 {
86
		return 0, fmt.Errorf("invalid duration %q", value)
87
	}
88
 
89
	var total time.Duration
90
	pos := 0
91
	for _, match := range matches {
92
		if match[0] != pos {
93
			return 0, fmt.Errorf("invalid duration %q", value)
94
		}
95
 
96
		numberText := value[match[2]:match[3]]
97
		unit := strings.ToLower(value[match[4]:match[5]])
98
		number, err := strconv.ParseFloat(numberText, 64)
99
		if err != nil {
100
			return 0, fmt.Errorf("invalid duration %q: %w", value, err)
101
		}
102
 
103
		var unitDuration time.Duration
104
		switch unit {
105
		case "ns":
106
			unitDuration = time.Nanosecond
107
		case "us", "µs":
108
			unitDuration = time.Microsecond
109
		case "ms":
110
			unitDuration = time.Millisecond
111
		case "s":
112
			unitDuration = time.Second
113
		case "m":
114
			unitDuration = time.Minute
115
		case "h":
116
			unitDuration = time.Hour
117
		case "d":
118
			unitDuration = 24 * time.Hour
119
		default:
120
			return 0, fmt.Errorf("invalid duration unit %q", unit)
121
		}
122
 
123
		part := number * float64(unitDuration)
124
		if part > float64(math.MaxInt64) {
125
			return 0, fmt.Errorf("duration %q is too large", value)
126
		}
127
		total += time.Duration(part)
128
		pos = match[1]
129
	}
130
 
131
	if pos != len(value) || total <= 0 {
132
		return 0, fmt.Errorf("invalid duration %q", value)
133
	}
134
	return total, nil
135
}
136
 
137
func FormatDuration(d time.Duration) string {
138
	if d < 0 {
139
		return "-" + FormatDuration(-d)
140
	}
141
	if d < time.Second {
142
		return d.String()
143
	}
144
 
145
	d = d.Round(time.Second)
146
	days := d / (24 * time.Hour)
147
	d -= days * 24 * time.Hour
148
	hours := d / time.Hour
149
	d -= hours * time.Hour
150
	minutes := d / time.Minute
151
	d -= minutes * time.Minute
152
	seconds := d / time.Second
153
 
154
	var parts []string
155
	if days > 0 {
156
		parts = append(parts, fmt.Sprintf("%dd", days))
157
	}
158
	if hours > 0 {
159
		parts = append(parts, fmt.Sprintf("%dh", hours))
160
	}
161
	if minutes > 0 {
162
		parts = append(parts, fmt.Sprintf("%dm", minutes))
163
	}
164
	if seconds > 0 || len(parts) == 0 {
165
		parts = append(parts, fmt.Sprintf("%ds", seconds))
166
	}
167
	return strings.Join(parts, " ")
168
}