| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551 |
- package executor
- import (
- "fmt"
- "math"
- "strconv"
- "strings"
- "time"
- )
- // julianDayToTime converts a Julian Day Number to time.Time (UTC).
- // Julian day 2440587.5 = 1970-01-01 00:00:00 UTC.
- func julianDayToTime(jd float64) time.Time {
- unixSec := (jd - 2440587.5) * 86400.0
- sec := int64(unixSec)
- nsec := int64(math.Round((unixSec - float64(sec)) * 1e9))
- if nsec < 0 {
- sec--
- nsec += 1e9
- }
- return time.Unix(sec, nsec).UTC()
- }
- func timeToJulianDay(t time.Time) float64 {
- return float64(t.UnixNano())/86400e9 + 2440587.5
- }
- // parseISO8601 parses the ISO-8601 subsets that SQLite supports.
- func parseISO8601(s string) (time.Time, error) {
- loc := time.UTC
- core := s
- // Strip trailing Z
- if len(core) > 0 && (core[len(core)-1] == 'Z' || core[len(core)-1] == 'z') {
- core = core[:len(core)-1]
- } else {
- // Strip ±HH:MM timezone suffix (only if after at least YYYY-MM-DD)
- if len(core) >= 16 {
- for i := len(core) - 6; i >= 10; i-- {
- if core[i] == '+' || core[i] == '-' {
- possible := core[i:]
- if len(possible) == 6 && possible[3] == ':' {
- var hh, mm int
- fmt.Sscanf(possible[1:], "%d:%d", &hh, &mm)
- sign := 1
- if possible[0] == '-' {
- sign = -1
- }
- offset := sign * (hh*3600 + mm*60)
- loc = time.FixedZone("", offset)
- core = core[:i]
- break
- }
- }
- }
- }
- }
- // Normalize T separator to space
- core = strings.Replace(core, "T", " ", 1)
- timeOnly := !strings.Contains(core, "-")
- layouts := []string{
- "2006-01-02 15:04:05.999999999",
- "2006-01-02 15:04:05",
- "2006-01-02 15:04",
- "2006-01-02",
- "15:04:05.999999999",
- "15:04:05",
- "15:04",
- }
- for _, layout := range layouts {
- t, err := time.ParseInLocation(layout, core, loc)
- if err == nil {
- if timeOnly {
- t = time.Date(2000, 1, 1, t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), loc)
- }
- return t.UTC(), nil
- }
- }
- return time.Time{}, fmt.Errorf("cannot parse time value: %q", s)
- }
- // parseDateArgs extracts time.Time and modifier strings from evaluated function args.
- // Handles 'now' default, numeric (Julian/Unix with modifier), and ISO-8601 text.
- func parseDateArgs(args []interface{}) (time.Time, []string, error) {
- if len(args) == 0 {
- return time.Now().UTC(), nil, nil
- }
- mods := make([]string, 0, len(args)-1)
- for _, a := range args[1:] {
- mods = append(mods, toString(a))
- }
- firstStr := strings.TrimSpace(toString(args[0]))
- firstLower := strings.ToLower(firstStr)
- // 'subsec'/'subsecond' as first arg means time-value defaults to 'now'
- if firstLower == "subsec" || firstLower == "subsecond" {
- return time.Now().UTC(), append([]string{firstStr}, mods...), nil
- }
- if firstLower == "now" {
- return time.Now().UTC(), mods, nil
- }
- // Numeric: Julian day by default; first modifier may change interpretation
- if f, err := strconv.ParseFloat(firstStr, 64); err == nil {
- if len(mods) > 0 {
- switch strings.ToLower(strings.TrimSpace(mods[0])) {
- case "unixepoch":
- sec := int64(f)
- nsec := int64(math.Round((f - float64(sec)) * 1e9))
- return time.Unix(sec, nsec).UTC(), mods[1:], nil
- case "auto":
- if f >= 0.0 && f <= 5373484.499999 {
- return julianDayToTime(f), mods[1:], nil
- }
- if f >= -210866760000 && f <= 253402300799 {
- return time.Unix(int64(f), 0).UTC(), mods[1:], nil
- }
- return time.Time{}, nil, fmt.Errorf("time value out of range for auto")
- case "julianday":
- return julianDayToTime(f), mods[1:], nil
- }
- }
- return julianDayToTime(f), mods, nil
- }
- // ISO-8601 text
- t, err := parseISO8601(firstStr)
- if err != nil {
- return time.Time{}, nil, err
- }
- return t, mods, nil
- }
- // applyModifiers applies SQLite date/time modifiers sequentially.
- // Returns modified time, subsec flag, and error.
- func applyModifiers(t time.Time, mods []string) (time.Time, bool, error) {
- subsec := false
- for _, mod := range mods {
- mod = strings.TrimSpace(mod)
- modLower := strings.ToLower(mod)
- switch modLower {
- case "subsec", "subsecond":
- subsec = true
- case "utc":
- t = t.UTC()
- case "localtime":
- t = t.Local()
- case "ceiling", "floor":
- // Affects ambiguous month-shift results; treated as no-op here
- case "unixepoch", "julianday", "auto":
- // Only valid as first modifier after numeric time-value; consumed by parseDateArgs
- case "start of month":
- t = time.Date(t.Year(), t.Month(), 1, 0, 0, 0, 0, t.Location())
- case "start of year":
- t = time.Date(t.Year(), 1, 1, 0, 0, 0, 0, t.Location())
- case "start of day":
- t = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location())
- default:
- if strings.HasPrefix(modLower, "weekday ") {
- nStr := strings.TrimPrefix(modLower, "weekday ")
- if n, err := strconv.Atoi(nStr); err == nil {
- target := time.Weekday(n % 7)
- for t.Weekday() != target {
- t = t.AddDate(0, 0, 1)
- }
- }
- continue
- }
- if t2, ok := applyRelativeMod(t, mod); ok {
- t = t2
- continue
- }
- if t2, ok := applyTimeDiffMod(t, mod); ok {
- t = t2
- }
- // Unknown modifiers are silently ignored (SQLite returns NULL; we're lenient)
- }
- }
- return t, subsec, nil
- }
- // applyRelativeMod handles "NNN days", "NNN hours", "NNN minutes", "NNN seconds",
- // "NNN months", "NNN years" (trailing 's' optional, sign prefix allowed).
- func applyRelativeMod(t time.Time, mod string) (time.Time, bool) {
- parts := strings.Fields(mod)
- if len(parts) != 2 {
- return t, false
- }
- f, err := strconv.ParseFloat(parts[0], 64)
- if err != nil {
- return t, false
- }
- unit := strings.ToLower(strings.TrimSuffix(parts[1], "s"))
- switch unit {
- case "day":
- return t.Add(time.Duration(f * float64(24*time.Hour))), true
- case "hour":
- return t.Add(time.Duration(f * float64(time.Hour))), true
- case "minute":
- return t.Add(time.Duration(f * float64(time.Minute))), true
- case "second":
- return t.Add(time.Duration(f * float64(time.Second))), true
- case "month":
- whole := int(f)
- frac := f - float64(whole)
- t = t.AddDate(0, whole, 0)
- if frac != 0 {
- t = t.Add(time.Duration(frac * float64(30*24*time.Hour)))
- }
- return t, true
- case "year":
- whole := int(f)
- frac := f - float64(whole)
- t = t.AddDate(whole, 0, 0)
- if frac != 0 {
- t = t.Add(time.Duration(frac * float64(365*24*time.Hour)))
- }
- return t, true
- }
- return t, false
- }
- // applyTimeDiffMod handles timediff-output style modifiers: ±YYYY-MM-DD HH:MM:SS.SSS
- func applyTimeDiffMod(t time.Time, mod string) (time.Time, bool) {
- if len(mod) == 0 {
- return t, false
- }
- sign := 1
- s := mod
- switch s[0] {
- case '+':
- s = s[1:]
- case '-':
- sign = -1
- s = s[1:]
- default:
- return t, false
- }
- parts := strings.SplitN(s, " ", 2)
- dateSubs := strings.Split(parts[0], "-")
- if len(dateSubs) != 3 {
- return t, false
- }
- years, e1 := strconv.Atoi(dateSubs[0])
- months, e2 := strconv.Atoi(dateSubs[1])
- days, e3 := strconv.Atoi(dateSubs[2])
- if e1 != nil || e2 != nil || e3 != nil {
- return t, false
- }
- hours, minutes, secs, millis := 0, 0, 0, 0
- if len(parts) == 2 {
- tp := strings.SplitN(parts[1], ":", 3)
- if len(tp) >= 1 {
- hours, _ = strconv.Atoi(tp[0])
- }
- if len(tp) >= 2 {
- minutes, _ = strconv.Atoi(tp[1])
- }
- if len(tp) >= 3 {
- sp := strings.SplitN(tp[2], ".", 2)
- secs, _ = strconv.Atoi(sp[0])
- if len(sp) > 1 {
- ms := sp[1]
- for len(ms) < 3 {
- ms += "0"
- }
- millis, _ = strconv.Atoi(ms[:3])
- }
- }
- }
- t = t.AddDate(sign*years, sign*months, sign*days)
- dur := time.Duration(sign) * (
- time.Duration(hours)*time.Hour +
- time.Duration(minutes)*time.Minute +
- time.Duration(secs)*time.Second +
- time.Duration(millis)*time.Millisecond)
- return t.Add(dur), true
- }
- // sqliteStrftime formats t using SQLite strftime codes.
- func sqliteStrftime(format string, t time.Time, subsec bool) string {
- var b strings.Builder
- jd := timeToJulianDay(t)
- for i := 0; i < len(format); i++ {
- if format[i] != '%' || i+1 >= len(format) {
- b.WriteByte(format[i])
- continue
- }
- i++
- switch format[i] {
- case 'd':
- fmt.Fprintf(&b, "%02d", t.Day())
- case 'e':
- fmt.Fprintf(&b, "%d", t.Day())
- case 'f':
- sec := float64(t.Second()) + float64(t.Nanosecond())/1e9
- fmt.Fprintf(&b, "%06.3f", sec)
- case 'F':
- fmt.Fprintf(&b, "%04d-%02d-%02d", t.Year(), int(t.Month()), t.Day())
- case 'G':
- y, _ := t.ISOWeek()
- fmt.Fprintf(&b, "%04d", y)
- case 'g':
- y, _ := t.ISOWeek()
- fmt.Fprintf(&b, "%02d", y%100)
- case 'H':
- fmt.Fprintf(&b, "%02d", t.Hour())
- case 'I':
- h := t.Hour() % 12
- if h == 0 {
- h = 12
- }
- fmt.Fprintf(&b, "%02d", h)
- case 'j':
- fmt.Fprintf(&b, "%03d", t.YearDay())
- case 'J':
- fmt.Fprintf(&b, "%.10f", jd)
- case 'k':
- fmt.Fprintf(&b, "%d", t.Hour())
- case 'l':
- h := t.Hour() % 12
- if h == 0 {
- h = 12
- }
- fmt.Fprintf(&b, "%d", h)
- case 'm':
- fmt.Fprintf(&b, "%02d", int(t.Month()))
- case 'M':
- fmt.Fprintf(&b, "%02d", t.Minute())
- case 'p':
- if t.Hour() < 12 {
- b.WriteString("AM")
- } else {
- b.WriteString("PM")
- }
- case 'P':
- if t.Hour() < 12 {
- b.WriteString("am")
- } else {
- b.WriteString("pm")
- }
- case 'R':
- fmt.Fprintf(&b, "%02d:%02d", t.Hour(), t.Minute())
- case 's':
- if subsec {
- fmt.Fprintf(&b, "%.3f", float64(t.Unix())+float64(t.Nanosecond())/1e9)
- } else {
- fmt.Fprintf(&b, "%d", t.Unix())
- }
- case 'S':
- fmt.Fprintf(&b, "%02d", t.Second())
- case 'T':
- fmt.Fprintf(&b, "%02d:%02d:%02d", t.Hour(), t.Minute(), t.Second())
- case 'U':
- fmt.Fprintf(&b, "%02d", sundayWeek(t))
- case 'u':
- w := int(t.Weekday())
- if w == 0 {
- w = 7
- }
- fmt.Fprintf(&b, "%d", w)
- case 'V':
- _, week := t.ISOWeek()
- fmt.Fprintf(&b, "%02d", week)
- case 'w':
- fmt.Fprintf(&b, "%d", int(t.Weekday()))
- case 'W':
- fmt.Fprintf(&b, "%02d", mondayWeek(t))
- case 'Y':
- fmt.Fprintf(&b, "%04d", t.Year())
- case '%':
- b.WriteByte('%')
- default:
- b.WriteByte('%')
- b.WriteByte(format[i])
- }
- }
- return b.String()
- }
- func sundayWeek(t time.Time) int {
- yd := t.YearDay()
- dow := int(t.Weekday()) // 0=Sunday
- return (yd - dow + 6) / 7
- }
- func mondayWeek(t time.Time) int {
- yd := t.YearDay()
- dow := int(t.Weekday())
- if dow == 0 {
- dow = 7
- }
- return (yd - dow + 7) / 7
- }
- func evalDateFunc(args []interface{}) (interface{}, error) {
- t, mods, err := parseDateArgs(args)
- if err != nil {
- return nil, nil
- }
- t, _, err = applyModifiers(t, mods)
- if err != nil {
- return nil, nil
- }
- return fmt.Sprintf("%04d-%02d-%02d", t.Year(), int(t.Month()), t.Day()), nil
- }
- func evalTimeFunc(args []interface{}) (interface{}, error) {
- t, mods, err := parseDateArgs(args)
- if err != nil {
- return nil, nil
- }
- t, subsec, err := applyModifiers(t, mods)
- if err != nil {
- return nil, nil
- }
- if subsec {
- return fmt.Sprintf("%02d:%02d:%02d.%03d", t.Hour(), t.Minute(), t.Second(), t.Nanosecond()/1e6), nil
- }
- return fmt.Sprintf("%02d:%02d:%02d", t.Hour(), t.Minute(), t.Second()), nil
- }
- func evalDatetimeFunc(args []interface{}) (interface{}, error) {
- t, mods, err := parseDateArgs(args)
- if err != nil {
- return nil, nil
- }
- t, subsec, err := applyModifiers(t, mods)
- if err != nil {
- return nil, nil
- }
- if subsec {
- return fmt.Sprintf("%04d-%02d-%02d %02d:%02d:%02d.%03d",
- t.Year(), int(t.Month()), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond()/1e6), nil
- }
- return fmt.Sprintf("%04d-%02d-%02d %02d:%02d:%02d",
- t.Year(), int(t.Month()), t.Day(), t.Hour(), t.Minute(), t.Second()), nil
- }
- func evalJuliandayFunc(args []interface{}) (interface{}, error) {
- t, mods, err := parseDateArgs(args)
- if err != nil {
- return nil, nil
- }
- t, _, err = applyModifiers(t, mods)
- if err != nil {
- return nil, nil
- }
- return timeToJulianDay(t), nil
- }
- func evalUnixepochFunc(args []interface{}) (interface{}, error) {
- t, mods, err := parseDateArgs(args)
- if err != nil {
- return nil, nil
- }
- t, subsec, err := applyModifiers(t, mods)
- if err != nil {
- return nil, nil
- }
- if subsec {
- return float64(t.Unix()) + float64(t.Nanosecond())/1e9, nil
- }
- return t.Unix(), nil
- }
- func evalStrftimeFunc(args []interface{}) (interface{}, error) {
- if len(args) == 0 {
- return nil, nil
- }
- format := toString(args[0])
- t, mods, err := parseDateArgs(args[1:])
- if err != nil {
- return nil, nil
- }
- t, subsec, err := applyModifiers(t, mods)
- if err != nil {
- return nil, nil
- }
- return sqliteStrftime(format, t, subsec), nil
- }
- // evalTimediffFunc implements timediff(A, B): returns ±YYYY-MM-DD HH:MM:SS.SSS
- // representing the amount of time to add to B to reach A.
- func evalTimediffFunc(args []interface{}) (interface{}, error) {
- if len(args) < 2 {
- return nil, nil
- }
- tA, _, err := parseDateArgs(args[0:1])
- if err != nil {
- return nil, nil
- }
- tB, _, err := parseDateArgs(args[1:2])
- if err != nil {
- return nil, nil
- }
- sign := "+"
- a, b := tA, tB
- if a.Before(b) {
- sign = "-"
- a, b = b, a
- }
- // Greedy calendar subtraction: find years, months, days, then sub-day duration.
- years := a.Year() - b.Year()
- cursor := b.AddDate(years, 0, 0)
- if cursor.After(a) {
- years--
- cursor = b.AddDate(years, 0, 0)
- }
- months := 0
- for cursor.AddDate(0, 1, 0).Before(a) || cursor.AddDate(0, 1, 0).Equal(a) {
- months++
- cursor = cursor.AddDate(0, 1, 0)
- }
- days := 0
- for cursor.AddDate(0, 0, 1).Before(a) || cursor.AddDate(0, 0, 1).Equal(a) {
- days++
- cursor = cursor.AddDate(0, 0, 1)
- }
- remaining := a.Sub(cursor)
- h := int(remaining.Hours())
- remaining -= time.Duration(h) * time.Hour
- m := int(remaining.Minutes())
- remaining -= time.Duration(m) * time.Minute
- s := int(remaining.Seconds())
- remaining -= time.Duration(s) * time.Second
- ms := int(remaining.Milliseconds())
- return fmt.Sprintf("%s%04d-%02d-%02d %02d:%02d:%02d.%03d",
- sign, years, months, days, h, m, s, ms), nil
- }
|