Skip to content

Commit e119bb4

Browse files
committed
Implement duration formatting.
1 parent 8b79e43 commit e119bb4

File tree

4 files changed

+210
-33
lines changed

4 files changed

+210
-33
lines changed

duration.go

+86-22
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,18 @@
11
package chrono
22

3-
import "math"
3+
import (
4+
"math"
5+
"strconv"
6+
)
47

58
type Duration struct {
6-
secs uint64
9+
secs int64
710
nsec uint32
811
}
912

10-
type Extent float64
11-
12-
// Common time-based durations relative to 1 nanosecond.
13-
const (
14-
Nanosecond Extent = 1
15-
Microsecond = 1000 * Nanosecond
16-
Millisecond = 1000 * Microsecond
17-
Second = 1000 * Millisecond
18-
Minute = 60 * Second
19-
Hour = 60 * Minute
20-
)
21-
2213
func DurationOf(v Extent) Duration {
23-
secs := v / 1e9
24-
if secs > math.MaxUint64 {
25-
panic("v overflows Duration")
26-
}
27-
2814
return Duration{
29-
secs: uint64(v / 1e9),
15+
secs: int64(v / 1e9),
3016
nsec: uint32(math.Mod(float64(v), 1e9)),
3117
}
3218
}
@@ -38,7 +24,7 @@ func (d Duration) Add(d2 Duration) Duration {
3824
}
3925

4026
return Duration{
41-
secs: uint64(nsec / 1e9),
27+
secs: int64(nsec / 1e9),
4228
nsec: uint32(math.Mod(nsec, 1e9)),
4329
}
4430
}
@@ -67,6 +53,84 @@ func (d Duration) Hours() float64 {
6753
return float64(d.secs)/(60*60) + float64(d.nsec)/(1e9*60*60)
6854
}
6955

56+
type Designator string
57+
58+
const (
59+
Hours Designator = "H"
60+
Minutes Designator = "M"
61+
Seconds Designator = "S"
62+
)
63+
64+
// Format the duration according to ISO 8601.
65+
// The output consists of only the time component - the period component is never included.
66+
// Thus the output always consists of "PT" followed by at least one unit of the time component (hours, minutes, seconds).
67+
//
68+
// The default format, obtained by calling the function with no arguments, consists of the most significant non-zero units,
69+
// presented non-breaking but trimmed.
70+
// 5 minutes is formatted as PT5M, trimming 0-value hours and seconds.
71+
// 1 hour and 5 seconds is formatted at PT1H0M5S, ensuring the sequence of units is not broken.
72+
//
73+
// A list of designators can be optionally passed to the function in order to configure which units are included.
74+
// When passed, only those specified units are included in the formatted string, and are present regardless of whether their value is 0.
75+
//
76+
// Fractional values are automatically applied to the least significant unit, if applicable.
77+
// In order to format only integers, the round functions should be used before calling this function.
78+
func (d Duration) Format(exclusive ...Designator) string {
79+
var values map[Designator]float64
80+
if len(exclusive) >= 1 {
81+
values = make(map[Designator]float64, 3)
82+
for _, d := range exclusive {
83+
values[d] = 0
84+
}
85+
}
86+
87+
_, h := values[Hours]
88+
_, m := values[Minutes]
89+
_, s := values[Seconds]
90+
91+
switch {
92+
case h && (m || s):
93+
values[Hours] = float64(d.secs / 3600)
94+
case h:
95+
values[Hours] = (float64(d.secs) / 3600) + (float64(d.nsec) / 3.6e12)
96+
}
97+
98+
switch {
99+
case m && s && h:
100+
values[Minutes] = float64((d.secs % 3600) / 60)
101+
case m && s:
102+
values[Minutes] = float64(d.secs / 60)
103+
case m && h:
104+
values[Minutes] = (float64(d.secs%3600) / 60) + (float64(d.nsec) / 6e10)
105+
case m:
106+
values[Minutes] = (float64(d.secs) / 60) + (float64(d.nsec) / 6e10)
107+
}
108+
109+
switch {
110+
case s && m:
111+
values[Seconds] = float64(d.secs%60) + (float64(d.nsec) / 1e9)
112+
case s && h:
113+
values[Seconds] = float64(d.secs%3600) + (float64(d.nsec) / 1e9)
114+
case s:
115+
values[Seconds] = float64(d.secs) + (float64(d.nsec) / 1e9)
116+
}
117+
118+
out := "PT"
119+
if v, ok := values[Hours]; ok {
120+
out += strconv.FormatFloat(v, 'f', -1, 64) + "H"
121+
}
122+
123+
if v, ok := values[Minutes]; ok {
124+
out += strconv.FormatFloat(v, 'f', -1, 64) + "M"
125+
}
126+
127+
if v, ok := values[Seconds]; ok {
128+
out += strconv.FormatFloat(v, 'f', -1, 64) + "S"
129+
}
130+
131+
return out
132+
}
133+
70134
// MinDuration returns the minimum allowed Duration of 0 ns.
71135
func MinDuration() *Duration {
72136
return &minDuration
@@ -79,4 +143,4 @@ func MaxDuration() *Duration {
79143

80144
var minDuration = Duration{secs: 0, nsec: 0}
81145

82-
var maxDuration = Duration{secs: math.MaxUint64, nsec: 1e9 - 1}
146+
var maxDuration = Duration{secs: math.MaxInt64, nsec: 1e9 - 1}

duration_test.go

+62-3
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import (
88
"github.com/go-chrono/chrono"
99
)
1010

11-
func TestOfDuration(t *testing.T) {
11+
func TestDurationOf(t *testing.T) {
1212
for _, tt := range []struct {
1313
name string
1414
of chrono.Extent
@@ -41,8 +41,8 @@ func TestOfDuration(t *testing.T) {
4141
},
4242
{
4343
name: "of hours",
44-
of: 2.5 * chrono.Hour,
45-
nsec: 9000000000000,
44+
of: 2 * chrono.Hour,
45+
nsec: 7200000000000,
4646
},
4747
} {
4848
t.Run(tt.name, func(t *testing.T) {
@@ -108,3 +108,62 @@ func TestDurationUnits(t *testing.T) {
108108
})
109109
}
110110
}
111+
112+
func TestDurationFormat(t *testing.T) {
113+
for _, tt := range []struct {
114+
name string
115+
of chrono.Extent
116+
exclusive []chrono.Designator
117+
expected string
118+
}{
119+
{
120+
name: "exclusive hms",
121+
of: 1*chrono.Hour + 15*chrono.Minute + 30*chrono.Second + 500*chrono.Millisecond,
122+
exclusive: []chrono.Designator{chrono.Hours, chrono.Minutes, chrono.Seconds},
123+
expected: "PT1H15M30.5S",
124+
},
125+
{
126+
name: "exclusive hm",
127+
of: 1*chrono.Hour + 15*chrono.Minute + 30*chrono.Second + 600*chrono.Millisecond,
128+
exclusive: []chrono.Designator{chrono.Hours, chrono.Minutes},
129+
expected: "PT1H15.51M",
130+
},
131+
{
132+
name: "exclusive hs",
133+
of: 12*chrono.Hour + 1*chrono.Minute + 30*chrono.Second + 500*chrono.Millisecond,
134+
exclusive: []chrono.Designator{chrono.Hours, chrono.Seconds},
135+
expected: "PT12H90.5S",
136+
},
137+
{
138+
name: "exclusive h",
139+
of: 1*chrono.Hour + 30*chrono.Minute + 36*chrono.Second + 36*chrono.Millisecond,
140+
exclusive: []chrono.Designator{chrono.Hours},
141+
expected: "PT1.51001H",
142+
},
143+
{
144+
name: "exclusive ms",
145+
of: 1*chrono.Hour + 15*chrono.Minute + 30*chrono.Second + 500*chrono.Millisecond,
146+
exclusive: []chrono.Designator{chrono.Minutes, chrono.Seconds},
147+
expected: "PT75M30.5S",
148+
},
149+
{
150+
name: "exclusive m",
151+
of: 1*chrono.Hour + 15*chrono.Minute + 30*chrono.Second + 600*chrono.Millisecond,
152+
exclusive: []chrono.Designator{chrono.Minutes},
153+
expected: "PT75.51M",
154+
},
155+
{
156+
name: "exclusive s",
157+
of: 1*chrono.Hour + 15*chrono.Minute + 30*chrono.Second + 500*chrono.Millisecond,
158+
exclusive: []chrono.Designator{chrono.Seconds},
159+
expected: "PT4530.5S",
160+
},
161+
} {
162+
t.Run(tt.name, func(t *testing.T) {
163+
d := chrono.DurationOf(tt.of)
164+
if out := d.Format(tt.exclusive...); out != tt.expected {
165+
t.Fatalf("formatted duration = %s, want %s", out, tt.expected)
166+
}
167+
})
168+
}
169+
}

extent.go

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package chrono
2+
3+
type Extent int64
4+
5+
// Common time-based durations relative to 1 nanosecond.
6+
const (
7+
Nanosecond Extent = 1
8+
Microsecond = 1000 * Nanosecond
9+
Millisecond = 1000 * Microsecond
10+
Second = 1000 * Millisecond
11+
Minute = 60 * Second
12+
Hour = 60 * Minute
13+
)
14+
15+
func (e Extent) Nanoseconds() int64 {
16+
return int64(e)
17+
}
18+
19+
func (e Extent) Microseconds() float64 {
20+
micros := e / Second
21+
nsec := e % Second
22+
return float64(micros) + float64(nsec)/1e3
23+
}
24+
25+
func (e Extent) Milliseconds() float64 {
26+
millis := e / Second
27+
nsec := e % Second
28+
return float64(millis) + float64(nsec)/1e6
29+
}
30+
31+
func (e Extent) Seconds() float64 {
32+
sec := e / Second
33+
nsec := e % Second
34+
return float64(sec) + float64(nsec)/1e9
35+
}
36+
37+
func (e Extent) Minutes() float64 {
38+
min := e / Minute
39+
nsec := e % Minute
40+
return float64(min) + float64(nsec)/(60*1e9)
41+
}
42+
43+
func (e Extent) Hours() float64 {
44+
hour := e / Hour
45+
nsec := e % Hour
46+
return float64(hour) + float64(nsec)/(60*60*1e9)
47+
}
48+
49+
func (e Extent) Truncate(m Extent) Extent {
50+
if e <= 0 {
51+
return e + e%m
52+
}
53+
return e - e%m
54+
}

instant.go

+8-8
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@ import "strconv"
44

55
// Instant represents an instantaneous point in time with nanosecond resolution.
66
type Instant struct {
7-
*int64
7+
v *int64
88
}
99

1010
// Now returns the Instant that represents the current point in time.
1111
func Now() Instant {
1212
now := monotime()
1313
return Instant{
14-
int64: &now,
14+
v: &now,
1515
}
1616
}
1717

@@ -21,32 +21,32 @@ func (i Instant) Elapsed() Duration {
2121
}
2222

2323
func (i Instant) String() string {
24-
return strconv.FormatInt(*i.int64, 10)
24+
return strconv.FormatInt(*i.v, 10)
2525
}
2626

2727
// Until returns the Duration that represents the elapsed time from i to v.
2828
func (i Instant) Until(v Instant) Duration {
2929
switch {
30-
case i.int64 == nil:
30+
case i.v == nil:
3131
panic("i is not initialized")
32-
case v.int64 == nil:
32+
case v.v == nil:
3333
panic("v is not initialized")
3434
}
3535

36-
iv, vv := *i.int64, *v.int64
36+
iv, vv := *i.v, *v.v
3737
if vv < iv {
3838
panic("v is smaller than i")
3939
}
4040

4141
d := vv - iv
4242
return Duration{
43-
secs: uint64(d / 1e9),
43+
secs: d / 1e9,
4444
nsec: uint32(d % 1e9),
4545
}
4646
}
4747

4848
func instant(t int64) Instant {
4949
return Instant{
50-
int64: &t,
50+
v: &t,
5151
}
5252
}

0 commit comments

Comments
 (0)