Skip to main content
Glama
orneryd

M.I.M.I.R - Multi-agent Intelligent Memory & Insight Repository

by orneryd
duration.go14 kB
// Duration and date/time handling for NornicDB Cypher. // // This file contains the CypherDuration type and functions for working with // temporal data in Cypher queries. CypherDuration is fully compatible with // Neo4j's duration type and supports ISO 8601 duration format. // // # Duration Format // // Durations use ISO 8601 format: P[n]Y[n]M[n]DT[n]H[n]M[n]S // // - P: Duration designator (required at start) // - Y: Years // - M: Months (before T) or Minutes (after T) // - D: Days // - T: Time designator (separates date from time components) // - H: Hours // - S: Seconds (can include fractional seconds) // // # Examples // // P1Y - 1 year // P3M - 3 months // P5D - 5 days // PT2H - 2 hours // PT30M - 30 minutes // PT45S - 45 seconds // P1Y2M3DT4H5M6S - 1 year, 2 months, 3 days, 4 hours, 5 minutes, 6 seconds // // # ELI12 // // Think of a duration like a recipe for time travel: // // "P1Y2M3D" = "Jump forward 1 year, 2 months, and 3 days" // "PT2H30M" = "Jump forward 2 hours and 30 minutes" // // The "P" at the start says "this is a time period" and the "T" in the // middle separates "calendar time" (years, months, days) from "clock time" // (hours, minutes, seconds). // // It's like giving directions: "Walk 3 blocks north, then turn and walk 2 blocks east" // becomes "Add 3 months, then add 2 hours". // // # Neo4j Compatibility // // CypherDuration matches Neo4j's duration semantics: // - Supports all ISO 8601 duration components // - Handles fractional seconds (nanosecond precision) // - Can be added to/subtracted from dates // - Supports duration arithmetic package cypher import ( "fmt" "regexp" "strconv" "strings" "time" ) // CypherDuration represents a Neo4j-compatible duration type. // // This type stores time intervals with separate components for calendar // units (years, months, days) and clock units (hours, minutes, seconds). // // Calendar units are stored separately because they have variable lengths: // - A year can be 365 or 366 days // - A month can be 28-31 days // // Clock units have fixed lengths and are stored precisely. // // # Fields // // - Years: Number of years (calendar unit) // - Months: Number of months (calendar unit) // - Days: Number of days (calendar unit) // - Hours: Number of hours (clock unit) // - Minutes: Number of minutes (clock unit) // - Seconds: Number of seconds (clock unit) // - Nanos: Nanoseconds for sub-second precision // // # Example // // dur := &CypherDuration{ // Years: 1, Months: 6, Days: 15, // Hours: 2, Minutes: 30, Seconds: 45, // } // fmt.Println(dur.String()) // P1Y6M15DT2H30M45S // // # ELI12 // // Think of CypherDuration like a time capsule with two compartments: // // 1. Calendar compartment: "How many birthdays/months/days to skip?" // (Years, Months, Days - these depend on the calendar) // // 2. Clock compartment: "How much to move the clock hands?" // (Hours, Minutes, Seconds - these are always the same length) // // We keep them separate because months aren't all the same length! // February has 28-29 days, while January has 31. type CypherDuration struct { Years int64 // Calendar years Months int64 // Calendar months Days int64 // Calendar days Hours int64 // Clock hours Minutes int64 // Clock minutes Seconds int64 // Clock seconds Nanos int64 // Sub-second precision (nanoseconds) } // String returns the duration in ISO 8601 format. // // The format is: P[n]Y[n]M[n]DT[n]H[n]M[n]S // // Only non-zero components are included. If no components are present, // returns "PT0S" (zero duration). // // # Example // // dur := &CypherDuration{Days: 5, Hours: 2} // fmt.Println(dur.String()) // "P5DT2H" // // zeroDur := &CypherDuration{} // fmt.Println(zeroDur.String()) // "PT0S" func (d *CypherDuration) String() string { var sb strings.Builder sb.WriteString("P") if d.Years > 0 { sb.WriteString(fmt.Sprintf("%dY", d.Years)) } if d.Months > 0 { sb.WriteString(fmt.Sprintf("%dM", d.Months)) } if d.Days > 0 { sb.WriteString(fmt.Sprintf("%dD", d.Days)) } if d.Hours > 0 || d.Minutes > 0 || d.Seconds > 0 || d.Nanos > 0 { sb.WriteString("T") if d.Hours > 0 { sb.WriteString(fmt.Sprintf("%dH", d.Hours)) } if d.Minutes > 0 { sb.WriteString(fmt.Sprintf("%dM", d.Minutes)) } if d.Seconds > 0 || d.Nanos > 0 { if d.Nanos > 0 { sb.WriteString(fmt.Sprintf("%d.%09dS", d.Seconds, d.Nanos)) } else { sb.WriteString(fmt.Sprintf("%dS", d.Seconds)) } } } if sb.Len() == 1 { sb.WriteString("T0S") // Empty duration } return sb.String() } // TotalDays returns the approximate total number of days. // // Note: This is an approximation because it assumes: // - 1 year = 365.25 days (average accounting for leap years) // - 1 month = 30.4375 days (365.25 / 12) // // For precise date calculations, use the individual components // with AddDurationToDate instead. // // # Example // // dur := &CypherDuration{Years: 1} // fmt.Println(dur.TotalDays()) // ~365.25 func (d *CypherDuration) TotalDays() float64 { // Approximate: 1 year = 365.25 days, 1 month = 30.4375 days return float64(d.Years)*365.25 + float64(d.Months)*30.4375 + float64(d.Days) + float64(d.Hours)/24 + float64(d.Minutes)/1440 + float64(d.Seconds)/86400 } // TotalSeconds returns the approximate total number of seconds. // // Note: This is an approximation for the calendar components. // See TotalDays for the assumptions used. // // # Example // // dur := &CypherDuration{Hours: 2, Minutes: 30} // fmt.Println(dur.TotalSeconds()) // 9000 func (d *CypherDuration) TotalSeconds() float64 { // Calculate directly to avoid double-counting from TotalDays return float64(d.Years)*365.25*86400 + float64(d.Months)*30.4375*86400 + float64(d.Days)*86400 + float64(d.Hours)*3600 + float64(d.Minutes)*60 + float64(d.Seconds) + float64(d.Nanos)/1e9 } // ToTimeDuration converts to Go's time.Duration. // // Note: This only converts the clock components (hours, minutes, seconds, nanos). // Calendar components (years, months, days) are converted to their approximate // durations which may not be accurate for actual date arithmetic. // // For precise date calculations, use AddDurationToDate instead. // // # Example // // dur := &CypherDuration{Hours: 2, Minutes: 30} // goDur := dur.ToTimeDuration() // fmt.Println(goDur) // 2h30m0s func (d *CypherDuration) ToTimeDuration() time.Duration { // Convert to time.Duration (approximate for calendar components) total := time.Duration(d.Days) * 24 * time.Hour total += time.Duration(d.Hours) * time.Hour total += time.Duration(d.Minutes) * time.Minute total += time.Duration(d.Seconds) * time.Second total += time.Duration(d.Nanos) * time.Nanosecond // Note: Years and months require calendar context for accurate conversion return total } // parseDuration parses an ISO 8601 duration string into a CypherDuration. // // Supported formats: // - ISO 8601: "P1Y2M3DT4H5M6S" // - With fractional seconds: "P1DT0.5S" // // # Parameters // // - s: The duration string to parse // // # Returns // // - A CypherDuration pointer, or nil if parsing fails // // # Example // // dur := parseDuration("P1Y2M3DT4H5M6S") // // dur.Years = 1, dur.Months = 2, dur.Days = 3 // // dur.Hours = 4, dur.Minutes = 5, dur.Seconds = 6 // // dur = parseDuration("PT2H30M") // // dur.Hours = 2, dur.Minutes = 30 func parseDuration(s string) *CypherDuration { s = strings.Trim(s, "'\"") if !strings.HasPrefix(strings.ToUpper(s), "P") { return nil } d := &CypherDuration{} s = strings.ToUpper(s[1:]) // Remove P prefix // Handle date part (before T) tIndex := strings.Index(s, "T") datePart := s timePart := "" if tIndex >= 0 { datePart = s[:tIndex] timePart = s[tIndex+1:] } // Parse date components re := regexp.MustCompile(`(\d+)([YMD])`) matches := re.FindAllStringSubmatch(datePart, -1) for _, match := range matches { val, _ := strconv.ParseInt(match[1], 10, 64) switch match[2] { case "Y": d.Years = val case "M": d.Months = val case "D": d.Days = val } } // Parse time components if timePart != "" { re = regexp.MustCompile(`(\d+\.?\d*)([HMS])`) matches = re.FindAllStringSubmatch(timePart, -1) for _, match := range matches { switch match[2] { case "H": val, _ := strconv.ParseInt(match[1], 10, 64) d.Hours = val case "M": val, _ := strconv.ParseInt(match[1], 10, 64) d.Minutes = val case "S": // Handle fractional seconds if strings.Contains(match[1], ".") { parts := strings.Split(match[1], ".") d.Seconds, _ = strconv.ParseInt(parts[0], 10, 64) // Convert fraction to nanoseconds frac := parts[1] for len(frac) < 9 { frac += "0" } d.Nanos, _ = strconv.ParseInt(frac[:9], 10, 64) } else { d.Seconds, _ = strconv.ParseInt(match[1], 10, 64) } } } } return d } // durationFromMap creates a CypherDuration from a map of components. // // This is used when duration components are specified as a map in Cypher: // // duration({days: 5, hours: 2}) // // # Parameters // // - m: A map with keys like "years", "months", "days", "hours", etc. // // # Returns // // - A CypherDuration populated from the map // // # Example // // m := map[string]interface{}{"days": 5, "hours": 2} // dur := durationFromMap(m) // // dur.Days = 5, dur.Hours = 2 func durationFromMap(m map[string]interface{}) *CypherDuration { d := &CypherDuration{} if v, ok := m["years"]; ok { d.Years = toInt64(v) } if v, ok := m["months"]; ok { d.Months = toInt64(v) } if v, ok := m["days"]; ok { d.Days = toInt64(v) } if v, ok := m["hours"]; ok { d.Hours = toInt64(v) } if v, ok := m["minutes"]; ok { d.Minutes = toInt64(v) } if v, ok := m["seconds"]; ok { d.Seconds = toInt64(v) } if v, ok := m["nanoseconds"]; ok { d.Nanos = toInt64(v) } return d } // durationBetween calculates the duration between two dates/datetimes. // // The result is always positive (absolute difference). // // # Parameters // // - d1: First date/datetime (string or time.Time) // - d2: Second date/datetime (string or time.Time) // // # Returns // // - A CypherDuration representing the difference, or nil if parsing fails // // # Example // // dur := durationBetween("2025-01-01", "2025-01-06") // // dur.Days = 5 // // dur = durationBetween("2025-01-01T10:00:00", "2025-01-01T12:30:00") // // dur.Hours = 2, dur.Minutes = 30 func durationBetween(d1, d2 interface{}) *CypherDuration { t1 := parseDateTime(d1) t2 := parseDateTime(d2) if t1.IsZero() || t2.IsZero() { return nil } diff := t2.Sub(t1) if diff < 0 { diff = -diff } return &CypherDuration{ Days: int64(diff.Hours()) / 24, Hours: int64(diff.Hours()) % 24, Minutes: int64(diff.Minutes()) % 60, Seconds: int64(diff.Seconds()) % 60, Nanos: int64(diff.Nanoseconds()) % 1e9, } } // parseDateTime parses various date/datetime formats. // // Supported formats: // - RFC3339: "2006-01-02T15:04:05Z07:00" // - ISO datetime: "2006-01-02T15:04:05" // - Datetime with space: "2006-01-02 15:04:05" // - Date only: "2006-01-02" // // # Parameters // // - v: A value to parse (string or time.Time) // // # Returns // // - A time.Time value, or zero time if parsing fails // // # Example // // t := parseDateTime("2025-01-15") // // t.Year() == 2025, t.Month() == January, t.Day() == 15 // // t = parseDateTime("2025-01-15T14:30:00") // // includes time components func parseDateTime(v interface{}) time.Time { switch val := v.(type) { case time.Time: return val case string: s := strings.Trim(val, "'\"") for _, layout := range []string{ time.RFC3339, "2006-01-02T15:04:05", "2006-01-02 15:04:05", "2006-01-02", } { if parsed, err := time.Parse(layout, s); err == nil { return parsed } } } return time.Time{} } // addDurationToDate adds a CypherDuration to a date/datetime. // // Calendar components (years, months, days) are added using calendar arithmetic, // correctly handling variable month lengths and leap years. Clock components // are added as fixed durations. // // # Parameters // // - dateVal: The base date/datetime // - dur: The duration to add // // # Returns // // - RFC3339 formatted result string, or empty string on error // // # Example // // result := addDurationToDate("2025-01-15", &CypherDuration{Days: 5}) // // result = "2025-01-20T00:00:00Z" // // result = addDurationToDate("2025-01-31", &CypherDuration{Months: 1}) // // result = "2025-02-28T00:00:00Z" (end of February) func addDurationToDate(dateVal interface{}, dur *CypherDuration) string { t := parseDateTime(dateVal) if t.IsZero() || dur == nil { return "" } // Add the duration components t = t.AddDate(int(dur.Years), int(dur.Months), int(dur.Days)) t = t.Add(time.Duration(dur.Hours)*time.Hour + time.Duration(dur.Minutes)*time.Minute + time.Duration(dur.Seconds)*time.Second + time.Duration(dur.Nanos)*time.Nanosecond) return t.Format(time.RFC3339) } // subtractDurationFromDate subtracts a CypherDuration from a date/datetime. // // This is the inverse of addDurationToDate. Calendar components are subtracted // using calendar arithmetic. // // # Parameters // // - dateVal: The base date/datetime // - dur: The duration to subtract // // # Returns // // - RFC3339 formatted result string, or empty string on error // // # Example // // result := subtractDurationFromDate("2025-01-15", &CypherDuration{Days: 5}) // // result = "2025-01-10T00:00:00Z" func subtractDurationFromDate(dateVal interface{}, dur *CypherDuration) string { t := parseDateTime(dateVal) if t.IsZero() || dur == nil { return "" } // Subtract the duration components t = t.AddDate(-int(dur.Years), -int(dur.Months), -int(dur.Days)) t = t.Add(-(time.Duration(dur.Hours)*time.Hour + time.Duration(dur.Minutes)*time.Minute + time.Duration(dur.Seconds)*time.Second + time.Duration(dur.Nanos)*time.Nanosecond)) return t.Format(time.RFC3339) }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/orneryd/Mimir'

If you have feedback or need assistance with the MCP directory API, please join our Discord server