Skip to content

Commit a8073ca

Browse files
authored
Support parsing & formatting with %f, %3f, %6f, and %9f (go-chrono#58)
* Support formatting with %f, %3f, %6f, and %9f. Signed-off-by: joe-mann <[email protected]> * Fix documentation. Signed-off-by: joe-mann <[email protected]> * Support parsing %f, %3f, %6f, and %9f. Signed-off-by: joe-mann <[email protected]> Signed-off-by: joe-mann <[email protected]>
1 parent 4e46672 commit a8073ca

File tree

5 files changed

+128
-23
lines changed

5 files changed

+128
-23
lines changed

export_test.go

+2
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,5 @@ func SetupCenturyParsing(v int) {
88
func TearDownCenturyParsing() {
99
overrideCentury = nil
1010
}
11+
12+
var DivideAndRoundIntFunc = divideAndRoundInt

format.go

+59-12
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@ import (
3535
// %M: The minute as a decimal number, padded to 2 digits with a leading 0, in the range 00 to 59.
3636
// %S: The second as a decimal number, padded to 2 digits with a leading 0, in the range 00 to 59.
3737
//
38+
// %f: Equivalent to %6f.
39+
// %3f: The millisecond offset within the represented second, rounded either up or down and padded to 3 digits with a leading 0.
40+
// %6f: The microsecond offset within the represented second, rounded either up or down and padded to 6 digits with a leading 0.
41+
// %9f: The nanosecond offset within the represented second, padded to 9 digits with a leading 0.
42+
//
3843
// When formatting using specifiers that represent padded decimals, leading 0s can be omitted using the '-' character after the '%'.
3944
// For example, '%m' may produce the string '04' (for March), but '%-m' produces '4'.
4045
// However, when parsing using these specifiers, it is not required that the input string contains any leading zeros.
@@ -110,11 +115,11 @@ NextChar:
110115
buf = append(buf, c)
111116

112117
if len(buf) >= 2 && buf[0] == '%' {
113-
if c == '-' || c == 'E' {
118+
if c == '-' || c == 'E' || (c >= '0' && c <= '9') {
114119
continue NextChar
115120
}
116121

117-
nopad, localed, main, err := parseSpecifier(buf)
122+
nopad, localed, precision, main, err := parseSpecifier(buf)
118123
if err != nil {
119124
return "", err
120125
}
@@ -147,6 +152,22 @@ NextChar:
147152
}
148153
case date != nil && main == 'd': // %d
149154
out = append(out, []rune(decimal(day, 2))...)
155+
case time != nil && main == 'f': // %f
156+
if precision == 0 {
157+
precision = 6
158+
}
159+
160+
nanos := time.Nanosecond()
161+
switch precision {
162+
case 3: // %3f
163+
out = append(out, []rune(decimal(divideAndRoundInt(nanos, 1000000), 3))...)
164+
case 6: // %6f
165+
out = append(out, []rune(decimal(divideAndRoundInt(nanos, 1000), 6))...)
166+
case 9: // %9f
167+
out = append(out, []rune(decimal(nanos, 9))...)
168+
default:
169+
panic(fmt.Sprintf("unsupported specifier '%df'", precision))
170+
}
150171
case date != nil && main == 'G': // %G
151172
y, _ := date.ISOWeek()
152173
out = append(out, []rune(decimal(y, 4))...)
@@ -347,7 +368,7 @@ func parse(layout, value string, date, time *int64) error {
347368
return string(_lower[:i]), string(_original[:i])
348369
}
349370

350-
_, localed, main, err := parseSpecifier(buf)
371+
_, localed, precision, main, err := parseSpecifier(buf)
351372
if err != nil {
352373
return err
353374
}
@@ -398,6 +419,30 @@ func parse(layout, value string, date, time *int64) error {
398419
if day, err = integer(2); err != nil {
399420
return err
400421
}
422+
case time != nil && main == 'f': // %f
423+
if precision == 0 {
424+
precision = 6
425+
}
426+
427+
switch precision {
428+
case 3: // %3f
429+
millis, err := integer(3)
430+
if err != nil {
431+
return err
432+
}
433+
nsec = millis * 1000000
434+
case 6: // %6f
435+
micros, err := integer(6)
436+
if err != nil {
437+
return err
438+
}
439+
nsec = micros * 1000
440+
case 9: // %9f
441+
if nsec, err = integer(9); err != nil {
442+
return err
443+
}
444+
default:
445+
}
401446
case date != nil && main == 'G': // %G
402447
haveISODate = true
403448
if isoYear, err = integer(4); err != nil {
@@ -488,7 +533,7 @@ func parse(layout, value string, date, time *int64) error {
488533
var (
489534
valid = i < len(layout)
490535
isSpecifier = len(buf) >= 2 && buf[0] == '%'
491-
specifierComplete = isSpecifier && (buf[len(buf)-1] != '-' && buf[len(buf)-1] != 'E')
536+
specifierComplete = isSpecifier && (buf[len(buf)-1] != '-' && buf[len(buf)-1] != 'E' && (buf[len(buf)-1] < '0' || buf[len(buf)-1] > '9'))
492537
isText = len(buf) >= 1 && buf[0] != '%'
493538
)
494539

@@ -612,32 +657,34 @@ func parse(layout, value string, date, time *int64) error {
612657
return nil
613658
}
614659

615-
func parseSpecifier(buf []rune) (nopad, localed bool, main rune, err error) {
660+
func parseSpecifier(buf []rune) (nopad, localed bool, precision uint, main rune, err error) {
616661
if len(buf) == 3 {
617-
switch buf[1] {
618-
case '-':
662+
switch {
663+
case buf[1] == '-':
619664
nopad = true
620-
case 'E':
665+
case buf[1] == 'E':
621666
localed = true
667+
case buf[1] >= '0' && buf[1] <= '9':
668+
precision = uint(buf[1] - 48)
622669
default:
623-
return false, false, 0, fmt.Errorf("unsupported modifier '%c'", buf[1])
670+
return false, false, 0, 0, fmt.Errorf("unsupported modifier '%c'", buf[1])
624671
}
625672
} else if len(buf) == 4 {
626673
switch buf[1] {
627674
case '-':
628675
nopad = true
629676
default:
630-
return false, false, 0, fmt.Errorf("unsupported modifier '%c'", buf[1])
677+
return false, false, 0, 0, fmt.Errorf("unsupported modifier '%c'", buf[1])
631678
}
632679

633680
switch buf[2] {
634681
case 'E':
635682
localed = true
636683
default:
637-
return false, false, 0, fmt.Errorf("unsupported modifier '%c'", buf[1])
684+
return false, false, 0, 0, fmt.Errorf("unsupported modifier '%c'", buf[1])
638685
}
639686
}
640-
return nopad, localed, buf[len(buf)-1], nil
687+
return nopad, localed, precision, buf[len(buf)-1], nil
641688
}
642689

643690
func convert12To24HourClock(hour12 int, isAfternoon bool) (hour24 int) {

format_test.go

+37-11
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,15 @@ import (
1010
)
1111

1212
const (
13-
formatYear = 807
14-
formatMonth = chrono.February
15-
formatDay = 9
16-
formatHour = 1
17-
formatMin = 5
18-
formatSec = 2
19-
formatNsec = 0
13+
formatYear = 807
14+
formatMonth = chrono.February
15+
formatDay = 9
16+
formatHour = 1
17+
formatMin = 5
18+
formatSec = 2
19+
formatMillis = 123
20+
formatMicros = 123457
21+
formatNanos = 123456789
2022
)
2123

2224
func setupCenturyParsing() {
@@ -96,6 +98,24 @@ func checkSecond(t *testing.T, time chrono.LocalTime) {
9698
}
9799
}
98100

101+
func checkMillis(t *testing.T, time chrono.LocalTime) {
102+
if nanos := time.Nanosecond(); nanos != formatMillis*1000000 {
103+
t.Errorf("time.Nanosecond() = %d, want %d", nanos, formatMillis*1000000)
104+
}
105+
}
106+
107+
func checkMicros(t *testing.T, time chrono.LocalTime) {
108+
if nanos := time.Nanosecond(); nanos != formatMicros*1000 {
109+
t.Errorf("time.Nanosecond() = %d, want %d", nanos, formatMillis*1000)
110+
}
111+
}
112+
113+
func checkNanos(t *testing.T, time chrono.LocalTime) {
114+
if nanos := time.Nanosecond(); nanos != formatNanos {
115+
t.Errorf("time.Nanosecond() = %d, want %d", nanos, formatNanos)
116+
}
117+
}
118+
99119
var (
100120
dateSpecifiers = []struct {
101121
specifier string
@@ -148,6 +168,10 @@ var (
148168
{"%-M", "5", checkMinute},
149169
{"%S", "02", checkSecond},
150170
{"%-S", "2", checkSecond},
171+
{"%3f", "123", checkMillis},
172+
{"%6f", "123457", checkMicros},
173+
{"%9f", "123456789", checkNanos},
174+
{"%f", "123457", checkMicros},
151175
}
152176
)
153177

@@ -276,14 +300,14 @@ func TestLocalTime_Format_supported_specifiers(t *testing.T) {
276300
}
277301
}()
278302

279-
chrono.LocalTimeOf(formatHour, formatMin, formatSec, formatNsec).Format(tt.specifier)
303+
chrono.LocalTimeOf(formatHour, formatMin, formatSec, formatNanos).Format(tt.specifier)
280304
}()
281305
})
282306
}
283307

284308
for _, tt := range timeSpecifiers {
285309
t.Run(tt.specifier, func(t *testing.T) {
286-
if formatted := chrono.LocalTimeOf(formatHour, formatMin, formatSec, formatNsec).Format(tt.specifier); formatted != tt.text {
310+
if formatted := chrono.LocalTimeOf(formatHour, formatMin, formatSec, formatNanos).Format(tt.specifier); formatted != tt.text {
287311
t.Errorf("time.Format(%s) = %s, want %q", tt.specifier, formatted, tt.text)
288312
}
289313
})
@@ -293,15 +317,17 @@ func TestLocalTime_Format_supported_specifiers(t *testing.T) {
293317
func TestLocalDateTime_Format_supported_specifiers(t *testing.T) {
294318
for _, tt := range dateSpecifiers {
295319
t.Run(fmt.Sprintf("%s (%q)", tt.specifier, tt.textToParse), func(t *testing.T) {
296-
if formatted := chrono.LocalDateTimeOf(formatYear, formatMonth, formatDay, formatHour, formatMin, formatSec, formatNsec).Format(tt.specifier); formatted != tt.expectedFormatted {
320+
if formatted := chrono.LocalDateTimeOf(formatYear, formatMonth, formatDay, formatHour, formatMin, formatSec, formatNanos).
321+
Format(tt.specifier); formatted != tt.expectedFormatted {
297322
t.Errorf("datetime.Format(%s) = %s, want %q", tt.specifier, formatted, tt.expectedFormatted)
298323
}
299324
})
300325
}
301326

302327
for _, tt := range timeSpecifiers {
303328
t.Run(tt.specifier, func(t *testing.T) {
304-
if formatted := chrono.LocalDateTimeOf(formatYear, formatMonth, formatDay, formatHour, formatMin, formatSec, formatNsec).Format(tt.specifier); formatted != tt.text {
329+
if formatted := chrono.LocalDateTimeOf(formatYear, formatMonth, formatDay, formatHour, formatMin, formatSec, formatNanos).
330+
Format(tt.specifier); formatted != tt.text {
305331
t.Errorf("datetime.Format(%s) = %s, want %q", tt.specifier, formatted, tt.text)
306332
}
307333
})

utils.go

+9
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,12 @@ func addInt64(v1, v2 int64) (sum int64, underflows, overflows bool) {
2525
}
2626
return v1 + v2, false, false
2727
}
28+
29+
// divideAndRoundInt divides x by y, then rounds the result to the nearest multiple of y, either up or down.
30+
func divideAndRoundInt(x, y int) int {
31+
r := x % y
32+
if r >= (y / 2) {
33+
return (x - r + y) / y
34+
}
35+
return (x - r) / y
36+
}

utils_test.go

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package chrono_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/go-chrono/chrono"
7+
)
8+
9+
func TestDivideAndRoundInt(t *testing.T) {
10+
t.Run("round up", func(t *testing.T) {
11+
if rounded := chrono.DivideAndRoundIntFunc(123456, 1000); rounded != 123 {
12+
t.Errorf("RoundInt() = %d, want %d", rounded, 123)
13+
}
14+
})
15+
16+
t.Run("round down", func(t *testing.T) {
17+
if rounded := chrono.DivideAndRoundIntFunc(123456789, 1000); rounded != 123457 {
18+
t.Errorf("RoundInt() = %d, want %d", rounded, 123457)
19+
}
20+
})
21+
}

0 commit comments

Comments
 (0)