// 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 eval import ( ) // MSPerDay is the milliseconds in a civil day. const MSPerDay int64 = 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 { return 0, true } if ( == math.MinInt64 && == -1) || ( == math.MinInt64 && == -1) { return 0, 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 { return 0, false } , := mulChk(DaysFromCivil(, , ), MSPerDay) if ! { return 0, false } return addChk(, ) } // 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 { case 1, 3, 5, 7, 8, 10, 12: return 31 case 4, 6, 9, 11: return 30 case 2: if (%4 == 0 && %100 != 0) || %400 == 0 { return 29 } return 28 } return 30 } // 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(, "-") if len() < 3 { return 0, false } , := parseI64([0]) , := parseI64([1]) , := parseI64([2]) if ! || ! || ! || < 1 || > 12 || < 1 || > 31 { return 0, false } var int64 if != "" { := strings.TrimSuffix(, "Z") if := strings.IndexByte(, '+'); >= 0 { = [:] } := strings.Split(, ":") , := parseI64([0]) if ! { return 0, false } var , , int64 if len() >= 2 { if , = parseI64([1]); ! { return 0, false } } if len() >= 3 { , , := strings.Cut([2], ".") if , = parseI64(); ! { return 0, false } if { := if len() > 3 { = [:3] } for len() < 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. return civilMillis(, 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 { return fmt.Sprintf("%04d-%02d-%02d", , , ) } := / 3_600_000 := ( / 60_000) % 60 := ( / 1000) % 60 if := % 1000; != 0 { return fmt.Sprintf("%04d-%02d-%02dT%02d:%02d:%02d.%03d", , , , , , , ) } return fmt.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'). := false if len() > 0 && ([0] == '-' || [0] == '+') { = [0] == '-' = [1:] } if len() < 2 || ([0] != 'P' && [0] != 'p') { return 0, 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) { return addChk(, ) } := func(, int64) (int64, bool) { return mulChk(, ) } for := 1; < len(); ++ { := [] switch { case >= '0' && <= '9': if < 0 { = 0 } , := (, 10) if ! { return 0, 0, 0, false } if , = (, int64(-'0')); ! { return 0, 0, 0, false } case == 'T' || == 't': if >= 0 || { return 0, 0, 0, false } = true case == '.': // A fraction is only valid on the seconds field: digits, then // 'S'. Millisecond precision (first three digits, zero-padded). if ! || < 0 || >= 0 { return 0, 0, 0, false } := + 1 , := int64(0), 0 for < len() && [] >= '0' && [] <= '9' { if < 3 { = *10 + int64([]-'0') ++ } ++ } if == 0 || >= len() || ([] != 'S' && [] != 's') { return 0, 0, 0, false } for < 3 { *= 10 ++ } = = - 1 // the loop's i++ lands on the 'S' default: if < 0 { return 0, 0, 0, false } var bool switch { case ! && ( == 'Y' || == 'y'): var int64 if , = (, 12); { , = (, ) } case == 'M' || == 'm': if { var int64 if , = (, 60_000); { , = (, ) } } else { , = (, ) } case ! && ( == 'W' || == 'w'): var int64 if , = (, 7); { , = (, ) } case ! && ( == 'D' || == 'd'): , = (, ) case && ( == 'H' || == 'h'): var int64 if , = (, 3_600_000); { , = (, ) } case && ( == 'S' || == 's'): var int64 if , = (, 1000); { if >= 0 { , = (, ) = -1 } if { , = (, ) } } default: return 0, 0, 0, false } if ! { return 0, 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 || ! { return 0, 0, 0, false } if { , , = -, -, - } return , , , true }