// Temporal arithmetic for the runtime value type: epoch-millis-backed// date/datetime values and durations, with civil-calendar decomposition.// Query-side only -- stored i64-millis columns coerce against these on// compare. Pure integer math (port of the Rust temporal.rs).
package evalimport ()// MSPerDay is the milliseconds in a civil day.constMSPerDayint64 = 86_400_000// maxCivilYear bounds a civil year to the epoch-millis representable range// (i64 milliseconds span roughly +/-292 million years). Beyond it the era// term in DaysFromCivil (era*146_097) or the later millisecond scaling// overflows silently, so temporal construction is bounded here first and// yields Null rather than a wrapped nonsense instant.const maxCivilYear int64 = 300_000_000// addChk / mulChk are checked int64 add / multiply (comma-ok), the temporal// path's guard against silent wrap -- overflow becomes Null, matching the// engine's integer-overflow policy.func addChk(, int64) (int64, bool) { := + return , ( > ) == ( > 0) || == 0}func mulChk(, int64) (int64, bool) {if == 0 || == 0 {return0, true }if ( == math.MinInt64 && == -1) || ( == math.MinInt64 && == -1) {return0, false } := * return , / == }// civilMillis converts a civil (year, month, day) plus a sub-day millisecond// offset to epoch millis, ok=false when the year is out of range or the// scaling overflows -- so a constructed temporal never wraps.func civilMillis( int64, , uint32, int64) (int64, bool) {if < -maxCivilYear || > maxCivilYear {return0, false } , := mulChk(DaysFromCivil(, , ), MSPerDay)if ! {return0, false }returnaddChk(, )}// DaysFromCivil is days since 1970-01-01 for a civil (year, month, day) --// Howard Hinnant's algorithm.func ( int64, , uint32) int64 {if <= 2 { -- } := if < 0 { = - 399 } /= 400 := - *400 := int64(+9) % 12 := (153*+2)/5 + int64() - 1 := *365 + /4 - /100 + return *146_097 + - 719_468}// CivilFromDays is the inverse: civil (year, month, day) from days since// 1970-01-01.func ( int64) ( int64, , uint32) { += 719_468 := if < 0 { = - 146_096 } /= 146_097 := - *146_097 := ( - /1460 + /36524 - /146_096) / 365 = + *400 := - (365* + /4 - /100) := (5* + 2) / 153 = uint32( - (153*+2)/5 + 1)if < 10 { = uint32( + 3) } else { = uint32( - 9) }if <= 2 { ++ }return , , }// DaysInMonth is the day count of (year, month), accounting for leap years// (used to clamp a calendar + duration).func ( int64, uint32) uint32 {switch {case1, 3, 5, 7, 8, 10, 12:return31case4, 6, 9, 11:return30case2:if (%4 == 0 && %100 != 0) || %400 == 0 {return29 }return28 }return30}// ParseISO parses an ISO-ish temporal string into epoch milliseconds// (UTC): YYYY-MM-DD, optionally THH:MM[:SS[.mmm]] and a trailing Z/offset// (the offset is accepted but treated as UTC -- no real zone handling).// ok is false on a malformed string.func ( string) (int64, bool) { = strings.TrimSpace() := := ""if := strings.IndexAny(, "T "); >= 0 { , = [:], [+1:] } := strings.Split(, "-")iflen() < 3 {return0, false } , := parseI64([0]) , := parseI64([1]) , := parseI64([2])if ! || ! || ! || < 1 || > 12 || < 1 || > 31 {return0, false }varint64if != "" { := strings.TrimSuffix(, "Z")if := strings.IndexByte(, '+'); >= 0 { = [:] } := strings.Split(, ":") , := parseI64([0])if ! {return0, false }var , , int64iflen() >= 2 {if , = parseI64([1]); ! {return0, false } }iflen() >= 3 { , , := strings.Cut([2], ".")if , = parseI64(); ! {return0, false }if { := iflen() > 3 { = [:3] }forlen() < 3 { += "0" }// A non-numeric fraction reads as 0, matching the Rust // parser's unwrap_or(0). , _ = parseI64() } } = *3_600_000 + *60_000 + *1000 + }// An absurd year overflows the era term / millisecond scaling; bound it // so a constructed instant never silently wraps.returncivilMillis(, uint32(), uint32(), )}// ISOString renders an epoch-millis temporal as ISO-8601, the inverse of// ParseISO: 'YYYY-MM-DD' for a Date; 'YYYY-MM-DDTHH:MM:SS' for the datetime// kinds, with '.mmm' appended only when the sub-second remainder is// non-zero. This is the single temporal string formatter -- any other// surface that stringifies a temporal should delegate here so the forms// cannot drift.func ( int64, value.TemporalKind) string { := floorDiv(, MSPerDay) := - *MSPerDay , , := CivilFromDays()if == value.Date {returnfmt.Sprintf("%04d-%02d-%02d", , , ) } := / 3_600_000 := ( / 60_000) % 60 := ( / 1000) % 60if := % 1000; != 0 {returnfmt.Sprintf("%04d-%02d-%02dT%02d:%02d:%02d.%03d", , , , , , , ) }returnfmt.Sprintf("%04d-%02d-%02dT%02d:%02d:%02d", , , , , , )}// ParseISODuration parses an ISO-8601 duration string (PnYnMnWnD[TnHnMnS],// e.g. 'P100D', 'PT12H', 'P1Y2M3DT4H5M6S') into calendar components:// months, days, and sub-day milliseconds. A leading '-'/'+' sign negates the// whole duration, and the seconds field accepts a millisecond fraction// ('PT1.5S'); ok is false on a malformed string or a component that overflows.func ( string) (, , int64, bool) { = strings.TrimSpace()// A leading sign negates the whole duration ('-P1D'). := falseiflen() > 0 && ([0] == '-' || [0] == '+') { = [0] == '-' = [1:] }iflen() < 2 || ([0] != 'P' && [0] != 'p') {return0, 0, 0, false } := false := false := int64(-1) := int64(-1) // parsed fractional-seconds millis, -1 = none// A field that overflows its unit conversion or the running total is a // wrapped nonsense duration; decline it (Null) rather than build one. := func(, int64) (int64, bool) { returnaddChk(, ) } := func(, int64) (int64, bool) { returnmulChk(, ) }for := 1; < len(); ++ { := []switch {case >= '0' && <= '9':if < 0 { = 0 } , := (, 10)if ! {return0, 0, 0, false }if , = (, int64(-'0')); ! {return0, 0, 0, false }case == 'T' || == 't':if >= 0 || {return0, 0, 0, false } = truecase == '.':// A fraction is only valid on the seconds field: digits, then // 'S'. Millisecond precision (first three digits, zero-padded).if ! || < 0 || >= 0 {return0, 0, 0, false } := + 1 , := int64(0), 0for < len() && [] >= '0' && [] <= '9' {if < 3 { = *10 + int64([]-'0') ++ } ++ }if == 0 || >= len() || ([] != 'S' && [] != 's') {return0, 0, 0, false }for < 3 { *= 10 ++ } = = - 1// the loop's i++ lands on the 'S'default:if < 0 {return0, 0, 0, false }varboolswitch {case ! && ( == 'Y' || == 'y'):varint64if , = (, 12); { , = (, ) }case == 'M' || == 'm':if {varint64if , = (, 60_000); { , = (, ) } } else { , = (, ) }case ! && ( == 'W' || == 'w'):varint64if , = (, 7); { , = (, ) }case ! && ( == 'D' || == 'd'): , = (, )case && ( == 'H' || == 'h'):varint64if , = (, 3_600_000); { , = (, ) }case && ( == 'S' || == 's'):varint64if , = (, 1000); {if >= 0 { , = (, ) = -1 }if { , = (, ) } }default:return0, 0, 0, false }if ! {return0, 0, 0, false } = true = -1 } }// A trailing bare number, an unconsumed fraction, or a designator-only // string ('P', 'PT') is malformed -- never a partial parse.if >= 0 || >= 0 || ! {return0, 0, 0, false }if { , , = -, -, - }return , , , true}
The pages are generated with Goldsv0.8.7. (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu.
PR and bug reports are welcome and can be submitted to the issue list.
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds.