Skip to content

Commit

Permalink
Remove base60 support for time.Duration
Browse files Browse the repository at this point in the history
By popular demand (or rather lack thereof), remove support
for the base60 representation of a time duration.
There is no standard for this format and even the name is an attempt
at naming something without clear industry basis for what this is called.

Updates golang/go#71631
  • Loading branch information
dsnet committed Feb 22, 2025
1 parent 925ba3f commit 2098e94
Show file tree
Hide file tree
Showing 5 changed files with 60 additions and 130 deletions.
4 changes: 0 additions & 4 deletions arshal.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,8 +155,6 @@ var export = jsontext.Internal.Export(&internal.AllowInternalUse)
// If the format is "sec", "milli", "micro", or "nano",
// then the duration is encoded as a JSON number of the number of seconds
// (or milliseconds, microseconds, or nanoseconds) in the duration.
// If the format is "base60", it is encoded as a JSON string
// using the "H:MM:SS.SSSSSSSSS" representation.
// If the format is "units", it uses [time.Duration.String].
//
// - All other Go types (e.g., complex numbers, channels, and functions)
Expand Down Expand Up @@ -385,8 +383,6 @@ func marshalEncode(out *jsontext.Encoder, in any, mo *jsonopts.Struct) (err erro
// If the format is "sec", "milli", "micro", or "nano",
// then the duration is decoded from a JSON number of the number of seconds
// (or milliseconds, microseconds, or nanoseconds) in the duration.
// If the format is "base60", it is decoded from a JSON string
// using the "H:MM:SS.SSSSSSSSS" representation.
// If the format is "units", it uses [time.ParseDuration].
//
// - All other Go types (e.g., complex numbers, channels, and functions)
Expand Down
7 changes: 2 additions & 5 deletions arshal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,6 @@ type (
D8 time.Duration `json:",string,format:micro"`
D9 time.Duration `json:",format:nano"`
D10 time.Duration `json:",string,format:nano"`
D11 time.Duration `json:",format:base60"`
}
structTimeFormat struct {
T1 time.Time
Expand Down Expand Up @@ -4373,7 +4372,6 @@ func TestMarshal(t *testing.T) {
12*time.Hour + 34*time.Minute + 56*time.Second + 78*time.Millisecond + 90*time.Microsecond + 12*time.Nanosecond,
12*time.Hour + 34*time.Minute + 56*time.Second + 78*time.Millisecond + 90*time.Microsecond + 12*time.Nanosecond,
12*time.Hour + 34*time.Minute + 56*time.Second + 78*time.Millisecond + 90*time.Microsecond + 12*time.Nanosecond,
12*time.Hour + 34*time.Minute + 56*time.Second + 78*time.Millisecond + 90*time.Microsecond + 12*time.Nanosecond,
},
want: `{
"D1": "12h34m56.078090012s",
Expand All @@ -4385,8 +4383,7 @@ func TestMarshal(t *testing.T) {
"D7": 45296078090.012,
"D8": "45296078090.012",
"D9": 45296078090012,
"D10": "45296078090012",
"D11": "12:34:56.078090012"
"D10": "45296078090012"
}`,
}, {
name: jsontest.Name("Duration/Format/Legacy"),
Expand All @@ -4395,7 +4392,7 @@ func TestMarshal(t *testing.T) {
D1: 12*time.Hour + 34*time.Minute + 56*time.Second + 78*time.Millisecond + 90*time.Microsecond + 12*time.Nanosecond,
D2: 12*time.Hour + 34*time.Minute + 56*time.Second + 78*time.Millisecond + 90*time.Microsecond + 12*time.Nanosecond,
},
want: `{"D1":45296078090012,"D2":"12h34m56.078090012s","D3":0,"D4":"0","D5":0,"D6":"0","D7":0,"D8":"0","D9":0,"D10":"0","D11":"0:00:00"}`,
want: `{"D1":45296078090012,"D2":"12h34m56.078090012s","D3":0,"D4":"0","D5":0,"D6":"0","D7":0,"D8":"0","D9":0,"D10":"0"}`,
}, {
name: jsontest.Name("Duration/MapKey"),
in: map[time.Duration]string{time.Second: ""},
Expand Down
46 changes: 0 additions & 46 deletions arshal_time.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,6 @@ type durationArshaler struct {
// - 0 uses time.Duration.String
// - 1e0, 1e3, 1e6, or 1e9 use a decimal encoding of the duration as
// nanoseconds, microseconds, milliseconds, or seconds.
// - 60 uses a "H:MM:SS.SSSSSSSSS" encoding
base uint64
}

Expand All @@ -214,8 +213,6 @@ func (a *durationArshaler) initFormat(format string) (ok bool) {
a.base = 1e3
case "nano":
a.base = 1e0
case "base60": // see https://en.wikipedia.org/wiki/Sexagesimal#Modern_usage
a.base = 60
default:
return false
}
Expand All @@ -230,8 +227,6 @@ func (a *durationArshaler) appendMarshal(b []byte) ([]byte, error) {
switch a.base {
case 0:
return append(b, a.td.String()...), nil
case 60:
return appendDurationBase60(b, a.td), nil
default:
return appendDurationBase10(b, a.td, a.base), nil
}
Expand All @@ -241,8 +236,6 @@ func (a *durationArshaler) unmarshal(b []byte) (err error) {
switch a.base {
case 0:
a.td, err = time.ParseDuration(string(b))
case 60:
a.td, err = parseDurationBase60(b)
default:
a.td, err = parseDurationBase10(b, a.base)
}
Expand Down Expand Up @@ -433,45 +426,6 @@ func parseDurationBase10(b []byte, pow10 uint64) (time.Duration, error) {
}
}

// appendDurationBase60 appends d formatted with H:MM:SS.SSS notation.
func appendDurationBase60(b []byte, d time.Duration) []byte {
b, n := mayAppendDurationSign(b, d) // append sign
n, nsec := bits.Div64(0, n, 1e9) // compute nsec field
n, sec := bits.Div64(0, n, 60) // compute sec field
hour, min := bits.Div64(0, n, 60) // compute hour and min fields
b = strconv.AppendUint(b, hour, 10) // append hour field
b = append(b, ':', '0'+byte(min/10), '0'+byte(min%10)) // append min field
b = append(b, ':', '0'+byte(sec/10), '0'+byte(sec%10)) // append sec field
return appendFracBase10(b, nsec, 1e9) // append nsec field
}

// parseDurationBase60 parses d formatted with H:MM:SS.SSS notation.
// The exact grammar is `-?(0|[1-9][0-9]*):[0-5][0-9]:[0-5][0-9]([.][0-9]+)?`.
func parseDurationBase60(b []byte) (time.Duration, error) {
checkBase60 := func(b []byte) bool {
return len(b) == 2 && ('0' <= b[0] && b[0] <= '5') && '0' <= b[1] && b[1] <= '9'
}
suffix, neg := consumeSign(b) // consume sign
hourBytes, suffix := bytesCutByte(suffix, ':', false) // consume hour field
minBytes, suffix := bytesCutByte(suffix, ':', false) // consume min field
secBytes, nsecBytes := bytesCutByte(suffix, '.', true) // consume sec and nsec fields
hour, okHour := jsonwire.ParseUint(hourBytes) // parse hour field; may overflow
min := parseDec2(minBytes) // parse min field
sec := parseDec2(secBytes) // parse sec field
nsec, okNsec := parseFracBase10(nsecBytes, 1e9) // parse nsec field
n := uint64(min)*60*1e9 + uint64(sec)*1e9 + uint64(nsec) // cannot overflow
hi, lo := bits.Mul64(hour, 60*60*1e9) // overflow if hi > 0
sum, co := bits.Add64(lo, n, 0) // overflow if co > 0
switch d := mayApplyDurationSign(sum, neg); { // overflow if neg != (d < 0)
case (!okHour && hour != math.MaxUint64) || !checkBase60(minBytes) || !checkBase60(secBytes) || !okNsec:
return 0, fmt.Errorf("invalid duration %q: %w", b, strconv.ErrSyntax)
case !okHour || hi > 0 || co > 0 || neg != (d < 0):
return 0, fmt.Errorf("invalid duration %q: %w", b, strconv.ErrRange)
default:
return d, nil
}
}

// mayAppendDurationSign appends a negative sign if n is negative.
func mayAppendDurationSign(b []byte, d time.Duration) ([]byte, uint64) {
if d < 0 {
Expand Down
128 changes: 57 additions & 71 deletions arshal_time_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,64 +26,63 @@ var formatDurationTestdata = []struct {
base10Milli string
base10Micro string
base10Nano string
base60 string
}{
{math.MaxInt64, "9223372036.854775807", "9223372036854.775807", "9223372036854775.807", "9223372036854775807", "2562047:47:16.854775807"},
{1e12 + 1e12, "2000", "2000000", "2000000000", "2000000000000", "0:33:20"},
{1e12 + 1e11, "1100", "1100000", "1100000000", "1100000000000", "0:18:20"},
{1e12 + 1e10, "1010", "1010000", "1010000000", "1010000000000", "0:16:50"},
{1e12 + 1e9, "1001", "1001000", "1001000000", "1001000000000", "0:16:41"},
{1e12 + 1e8, "1000.1", "1000100", "1000100000", "1000100000000", "0:16:40.1"},
{1e12 + 1e7, "1000.01", "1000010", "1000010000", "1000010000000", "0:16:40.01"},
{1e12 + 1e6, "1000.001", "1000001", "1000001000", "1000001000000", "0:16:40.001"},
{1e12 + 1e5, "1000.0001", "1000000.1", "1000000100", "1000000100000", "0:16:40.0001"},
{1e12 + 1e4, "1000.00001", "1000000.01", "1000000010", "1000000010000", "0:16:40.00001"},
{1e12 + 1e3, "1000.000001", "1000000.001", "1000000001", "1000000001000", "0:16:40.000001"},
{1e12 + 1e2, "1000.0000001", "1000000.0001", "1000000000.1", "1000000000100", "0:16:40.0000001"},
{1e12 + 1e1, "1000.00000001", "1000000.00001", "1000000000.01", "1000000000010", "0:16:40.00000001"},
{1e12 + 1e0, "1000.000000001", "1000000.000001", "1000000000.001", "1000000000001", "0:16:40.000000001"},
{+(1e9 + 1), "1.000000001", "1000.000001", "1000000.001", "1000000001", "0:00:01.000000001"},
{+(1e9), "1", "1000", "1000000", "1000000000", "0:00:01"},
{+(1e9 - 1), "0.999999999", "999.999999", "999999.999", "999999999", "0:00:00.999999999"},
{+100000000, "0.1", "100", "100000", "100000000", "0:00:00.1"},
{+120000000, "0.12", "120", "120000", "120000000", "0:00:00.12"},
{+123000000, "0.123", "123", "123000", "123000000", "0:00:00.123"},
{+123400000, "0.1234", "123.4", "123400", "123400000", "0:00:00.1234"},
{+123450000, "0.12345", "123.45", "123450", "123450000", "0:00:00.12345"},
{+123456000, "0.123456", "123.456", "123456", "123456000", "0:00:00.123456"},
{+123456700, "0.1234567", "123.4567", "123456.7", "123456700", "0:00:00.1234567"},
{+123456780, "0.12345678", "123.45678", "123456.78", "123456780", "0:00:00.12345678"},
{+123456789, "0.123456789", "123.456789", "123456.789", "123456789", "0:00:00.123456789"},
{+12345678, "0.012345678", "12.345678", "12345.678", "12345678", "0:00:00.012345678"},
{+1234567, "0.001234567", "1.234567", "1234.567", "1234567", "0:00:00.001234567"},
{+123456, "0.000123456", "0.123456", "123.456", "123456", "0:00:00.000123456"},
{+12345, "0.000012345", "0.012345", "12.345", "12345", "0:00:00.000012345"},
{+1234, "0.000001234", "0.001234", "1.234", "1234", "0:00:00.000001234"},
{+123, "0.000000123", "0.000123", "0.123", "123", "0:00:00.000000123"},
{+12, "0.000000012", "0.000012", "0.012", "12", "0:00:00.000000012"},
{+1, "0.000000001", "0.000001", "0.001", "1", "0:00:00.000000001"},
{0, "0", "0", "0", "0", "0:00:00"},
{-1, "-0.000000001", "-0.000001", "-0.001", "-1", "-0:00:00.000000001"},
{-12, "-0.000000012", "-0.000012", "-0.012", "-12", "-0:00:00.000000012"},
{-123, "-0.000000123", "-0.000123", "-0.123", "-123", "-0:00:00.000000123"},
{-1234, "-0.000001234", "-0.001234", "-1.234", "-1234", "-0:00:00.000001234"},
{-12345, "-0.000012345", "-0.012345", "-12.345", "-12345", "-0:00:00.000012345"},
{-123456, "-0.000123456", "-0.123456", "-123.456", "-123456", "-0:00:00.000123456"},
{-1234567, "-0.001234567", "-1.234567", "-1234.567", "-1234567", "-0:00:00.001234567"},
{-12345678, "-0.012345678", "-12.345678", "-12345.678", "-12345678", "-0:00:00.012345678"},
{-123456789, "-0.123456789", "-123.456789", "-123456.789", "-123456789", "-0:00:00.123456789"},
{-123456780, "-0.12345678", "-123.45678", "-123456.78", "-123456780", "-0:00:00.12345678"},
{-123456700, "-0.1234567", "-123.4567", "-123456.7", "-123456700", "-0:00:00.1234567"},
{-123456000, "-0.123456", "-123.456", "-123456", "-123456000", "-0:00:00.123456"},
{-123450000, "-0.12345", "-123.45", "-123450", "-123450000", "-0:00:00.12345"},
{-123400000, "-0.1234", "-123.4", "-123400", "-123400000", "-0:00:00.1234"},
{-123000000, "-0.123", "-123", "-123000", "-123000000", "-0:00:00.123"},
{-120000000, "-0.12", "-120", "-120000", "-120000000", "-0:00:00.12"},
{-100000000, "-0.1", "-100", "-100000", "-100000000", "-0:00:00.1"},
{-(1e9 - 1), "-0.999999999", "-999.999999", "-999999.999", "-999999999", "-0:00:00.999999999"},
{-(1e9), "-1", "-1000", "-1000000", "-1000000000", "-0:00:01"},
{-(1e9 + 1), "-1.000000001", "-1000.000001", "-1000000.001", "-1000000001", "-0:00:01.000000001"},
{math.MinInt64, "-9223372036.854775808", "-9223372036854.775808", "-9223372036854775.808", "-9223372036854775808", "-2562047:47:16.854775808"},
{math.MaxInt64, "9223372036.854775807", "9223372036854.775807", "9223372036854775.807", "9223372036854775807"},
{1e12 + 1e12, "2000", "2000000", "2000000000", "2000000000000"},
{1e12 + 1e11, "1100", "1100000", "1100000000", "1100000000000"},
{1e12 + 1e10, "1010", "1010000", "1010000000", "1010000000000"},
{1e12 + 1e9, "1001", "1001000", "1001000000", "1001000000000"},
{1e12 + 1e8, "1000.1", "1000100", "1000100000", "1000100000000"},
{1e12 + 1e7, "1000.01", "1000010", "1000010000", "1000010000000"},
{1e12 + 1e6, "1000.001", "1000001", "1000001000", "1000001000000"},
{1e12 + 1e5, "1000.0001", "1000000.1", "1000000100", "1000000100000"},
{1e12 + 1e4, "1000.00001", "1000000.01", "1000000010", "1000000010000"},
{1e12 + 1e3, "1000.000001", "1000000.001", "1000000001", "1000000001000"},
{1e12 + 1e2, "1000.0000001", "1000000.0001", "1000000000.1", "1000000000100"},
{1e12 + 1e1, "1000.00000001", "1000000.00001", "1000000000.01", "1000000000010"},
{1e12 + 1e0, "1000.000000001", "1000000.000001", "1000000000.001", "1000000000001"},
{+(1e9 + 1), "1.000000001", "1000.000001", "1000000.001", "1000000001"},
{+(1e9), "1", "1000", "1000000", "1000000000"},
{+(1e9 - 1), "0.999999999", "999.999999", "999999.999", "999999999"},
{+100000000, "0.1", "100", "100000", "100000000"},
{+120000000, "0.12", "120", "120000", "120000000"},
{+123000000, "0.123", "123", "123000", "123000000"},
{+123400000, "0.1234", "123.4", "123400", "123400000"},
{+123450000, "0.12345", "123.45", "123450", "123450000"},
{+123456000, "0.123456", "123.456", "123456", "123456000"},
{+123456700, "0.1234567", "123.4567", "123456.7", "123456700"},
{+123456780, "0.12345678", "123.45678", "123456.78", "123456780"},
{+123456789, "0.123456789", "123.456789", "123456.789", "123456789"},
{+12345678, "0.012345678", "12.345678", "12345.678", "12345678"},
{+1234567, "0.001234567", "1.234567", "1234.567", "1234567"},
{+123456, "0.000123456", "0.123456", "123.456", "123456"},
{+12345, "0.000012345", "0.012345", "12.345", "12345"},
{+1234, "0.000001234", "0.001234", "1.234", "1234"},
{+123, "0.000000123", "0.000123", "0.123", "123"},
{+12, "0.000000012", "0.000012", "0.012", "12"},
{+1, "0.000000001", "0.000001", "0.001", "1"},
{0, "0", "0", "0", "0"},
{-1, "-0.000000001", "-0.000001", "-0.001", "-1"},
{-12, "-0.000000012", "-0.000012", "-0.012", "-12"},
{-123, "-0.000000123", "-0.000123", "-0.123", "-123"},
{-1234, "-0.000001234", "-0.001234", "-1.234", "-1234"},
{-12345, "-0.000012345", "-0.012345", "-12.345", "-12345"},
{-123456, "-0.000123456", "-0.123456", "-123.456", "-123456"},
{-1234567, "-0.001234567", "-1.234567", "-1234.567", "-1234567"},
{-12345678, "-0.012345678", "-12.345678", "-12345.678", "-12345678"},
{-123456789, "-0.123456789", "-123.456789", "-123456.789", "-123456789"},
{-123456780, "-0.12345678", "-123.45678", "-123456.78", "-123456780"},
{-123456700, "-0.1234567", "-123.4567", "-123456.7", "-123456700"},
{-123456000, "-0.123456", "-123.456", "-123456", "-123456000"},
{-123450000, "-0.12345", "-123.45", "-123450", "-123450000"},
{-123400000, "-0.1234", "-123.4", "-123400", "-123400000"},
{-123000000, "-0.123", "-123", "-123000", "-123000000"},
{-120000000, "-0.12", "-120", "-120000", "-120000000"},
{-100000000, "-0.1", "-100", "-100000", "-100000000"},
{-(1e9 - 1), "-0.999999999", "-999.999999", "-999999.999", "-999999999"},
{-(1e9), "-1", "-1000", "-1000000", "-1000000000"},
{-(1e9 + 1), "-1.000000001", "-1000.000001", "-1000000.001", "-1000000001"},
{math.MinInt64, "-9223372036.854775808", "-9223372036854.775808", "-9223372036854775.808", "-9223372036854775808"},
}

func TestFormatDuration(t *testing.T) {
Expand All @@ -106,7 +105,6 @@ func TestFormatDuration(t *testing.T) {
check(tt.td, tt.base10Milli, 1e6)
check(tt.td, tt.base10Micro, 1e3)
check(tt.td, tt.base10Nano, 1e0)
check(tt.td, tt.base60, 60)
}
}

Expand Down Expand Up @@ -139,18 +137,6 @@ var parseDurationTestdata = []struct {
{"-1.0009", 1e3, -time.Microsecond, false},
{"-1.0000009", 1e6, -time.Millisecond, false},
{"-1.0000000009", 1e9, -time.Second, false},
{"1:23:45", 60, time.Hour + 23*time.Minute + 45*time.Second, false},
{"1:60:45", 60, 0, true},
{"1:23:60", 60, 0, true},
{"1:23:45.", 60, 0, true},
{"1:23:45.0", 60, time.Hour + 23*time.Minute + 45*time.Second, false},
{"1:23:45.1234567899", 60, time.Hour + 23*time.Minute + 45*time.Second + 123456789*time.Nanosecond, false},
{"1:23:45.123456789x", 60, 0, true},
{"23:45", 60, 0, true},
{"45", 60, 0, true},
{"00:00:00", 60, 0, true},
{"2562047:47:16.854775808", 60, 0, true},
{"2562048:00:00", 60, 0, true},
}

func TestParseDuration(t *testing.T) {
Expand All @@ -173,7 +159,7 @@ func FuzzFormatDuration(f *testing.F) {
}
f.Fuzz(func(t *testing.T, want int64) {
var buf []byte
for _, base := range [...]uint64{1e0, 1e3, 1e6, 1e9, 60} {
for _, base := range [...]uint64{1e0, 1e3, 1e6, 1e9} {
a := durationArshaler{td: time.Duration(want), base: base}
buf, _ = a.appendMarshal(buf[:0])
switch err := a.unmarshal(buf); {
Expand Down
5 changes: 1 addition & 4 deletions example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -410,7 +410,6 @@ func Example_formatFlags() {
TimeUnixSec time.Time `json:",format:unix"`
DurationSecs time.Duration `json:",format:sec"`
DurationNanos time.Duration `json:",format:nano"`
DurationBase60 time.Duration `json:",format:base60"`
}{
BytesBase64: []byte{0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef},
BytesHex: [8]byte{0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef},
Expand All @@ -422,7 +421,6 @@ func Example_formatFlags() {
TimeUnixSec: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC),
DurationSecs: 12*time.Hour + 34*time.Minute + 56*time.Second + 7*time.Millisecond + 8*time.Microsecond + 9*time.Nanosecond,
DurationNanos: 12*time.Hour + 34*time.Minute + 56*time.Second + 7*time.Millisecond + 8*time.Microsecond + 9*time.Nanosecond,
DurationBase60: 12*time.Hour + 34*time.Minute + 56*time.Second + 7*time.Millisecond + 8*time.Microsecond + 9*time.Nanosecond,
}

b, err := json.Marshal(&value)
Expand Down Expand Up @@ -452,8 +450,7 @@ func Example_formatFlags() {
// "TimeDateOnly": "2000-01-01",
// "TimeUnixSec": 946684800,
// "DurationSecs": 45296.007008009,
// "DurationNanos": 45296007008009,
// "DurationBase60": "12:34:56.007008009"
// "DurationNanos": 45296007008009
// }
}

Expand Down

0 comments on commit 2098e94

Please sign in to comment.