{"version":3,"file":"datetime_math.mjs","names":[],"sources":["../../../src/batteries/tools/datetime_math/index.ts"],"sourcesContent":["/**\n * Pre-constructed tools for ISO datetime arithmetic, differences, and timezone-aware formatting.\n *\n * @module @nhtio/adk/batteries/tools/datetime_math\n *\n * @remarks\n * Pre-constructed bundled tools for the `datetime_math` category. Import individually, the whole\n * category, or import every tool via `@nhtio/adk/batteries`.\n */\n\nimport { Tool } from '@nhtio/adk/common'\nimport { validator } from '@nhtio/validation'\nimport { DateTime, Duration, IANAZone } from 'luxon'\n\nfunction resolveZone(timezone: string | undefined): { zone: string; error?: string } {\n  if (!timezone) return { zone: 'UTC' }\n  if (!IANAZone.isValidZone(timezone)) return { zone: '', error: `Invalid timezone \"${timezone}\".` }\n  return { zone: timezone }\n}\n\nfunction parseDate(input: string, zone: string): DateTime | { error: string } {\n  if (input.toLowerCase() === 'now') return DateTime.now().setZone(zone)\n  const dt = DateTime.fromISO(input, { zone })\n  if (!dt.isValid)\n    return {\n      error: `Invalid date \"${input}\". Use ISO 8601 format (e.g. \"2025-06-15\" or \"2025-06-15T14:30:00\") or \"now\".`,\n    }\n  return dt\n}\n\n/**\n * Add or subtract a duration from a date/time.\n *\n * @remarks\n * All duration components (years/months/weeks/days/hours/minutes/seconds) are optional and\n * combined into a single Duration. Output formatting includes a time component when the input\n * itself has one or when any sub-day component is non-zero.\n */\nexport const dateAddTool = new Tool({\n  name: 'date_add',\n  description:\n    'Add or subtract a duration from a date/time. Useful for \"what date is 90 days from now?\" or \"when was 6 months before X?\"',\n  inputSchema: validator.object({\n    date: validator.string().required().description('ISO 8601 date/datetime string or \"now\"'),\n    direction: validator\n      .string()\n      .valid('add', 'subtract')\n      .required()\n      .description('\"add\" to move forward in time, \"subtract\" to move backward'),\n    years: validator.number().default(0).description('Years component (optional)'),\n    months: validator.number().default(0).description('Months component (optional)'),\n    weeks: validator.number().default(0).description('Weeks component (optional)'),\n    days: validator.number().default(0).description('Days component (optional)'),\n    hours: validator.number().default(0).description('Hours component (optional)'),\n    minutes: validator.number().default(0).description('Minutes component (optional)'),\n    seconds: validator.number().default(0).description('Seconds component (optional)'),\n    timezone: validator\n      .string()\n      .optional()\n      .allow('')\n      .description(\n        'IANA timezone for interpreting the date. Omit or send an empty string to use UTC.'\n      ),\n  }),\n  handler: async (args) => {\n    const { date, direction, years, months, weeks, days, hours, minutes, seconds, timezone } =\n      args as {\n        date: string\n        direction: 'add' | 'subtract'\n        years: number\n        months: number\n        weeks: number\n        days: number\n        hours: number\n        minutes: number\n        seconds: number\n        timezone?: string\n      }\n    const { zone, error: zoneError } = resolveZone(timezone)\n    if (zoneError) return `Error: ${zoneError}`\n\n    const parsed = parseDate(date, zone)\n    if ('error' in parsed) return `Error: ${parsed.error}`\n\n    const durObj = { years, months, weeks, days, hours, minutes, seconds }\n    const dur = Duration.fromObject(durObj)\n    const result = direction === 'subtract' ? parsed.minus(dur) : parsed.plus(dur)\n\n    const inputHasTime = date.includes('T') || date.toLowerCase() === 'now'\n    const durationHasTime = durObj.hours !== 0 || durObj.minutes !== 0 || durObj.seconds !== 0\n    const showTime = inputHasTime || durationHasTime\n\n    const formatted = showTime\n      ? result.toFormat(\"cccc, LLLL d, yyyy 'at' h:mm:ss a ZZZZ\")\n      : result.toFormat('cccc, LLLL d, yyyy')\n\n    const durParts = Object.entries(durObj)\n      .filter(([, v]) => v !== 0)\n      .map(([k, v]) => `${v} ${k}`)\n      .join(', ')\n\n    const verb = direction === 'subtract' ? 'Subtracting' : 'Adding'\n    const prep = direction === 'subtract' ? 'from' : 'to'\n    return `${verb} ${durParts || '0'} ${prep} ${date}: ${formatted}`\n  },\n})\n\n/**\n * Calculate the difference between two dates/times in a chosen unit.\n *\n * @remarks\n * Result is signed — positive when `to` is after `from`, negative otherwise — but rendered as\n * `|value| <unit> after/before` for readability. Uses luxon's `diff().as(unit)` which respects\n * calendar arithmetic for months/years.\n */\nexport const dateDiffTool = new Tool({\n  name: 'date_diff',\n  description:\n    'Calculate the difference between two dates/times in a specified unit. Useful for \"how many days until X?\" or \"how long ago was Y?\"',\n  inputSchema: validator.object({\n    from: validator.string().required().description('Start date (ISO 8601 or \"now\")'),\n    to: validator.string().required().description('End date (ISO 8601 or \"now\")'),\n    unit: validator\n      .string()\n      .valid('years', 'months', 'weeks', 'days', 'hours', 'minutes', 'seconds')\n      .required()\n      .description('Unit to express the difference in'),\n    timezone: validator\n      .string()\n      .optional()\n      .allow('')\n      .description(\n        'IANA timezone for interpreting dates. Omit or send an empty string to use UTC.'\n      ),\n  }),\n  handler: async (args) => {\n    const { from, to, unit, timezone } = args as {\n      from: string\n      to: string\n      unit: 'years' | 'months' | 'weeks' | 'days' | 'hours' | 'minutes' | 'seconds'\n      timezone?: string\n    }\n    const { zone, error: zoneError } = resolveZone(timezone)\n    if (zoneError) return `Error: ${zoneError}`\n\n    const fromParsed = parseDate(from, zone)\n    if ('error' in fromParsed) return `Error: ${fromParsed.error}`\n\n    const toParsed = parseDate(to, zone)\n    if ('error' in toParsed) return `Error: ${toParsed.error}`\n\n    const diff = toParsed.diff(fromParsed, unit)\n    const value = diff.as(unit)\n    const rounded = Number.parseFloat(value.toFixed(4))\n    const abs = Math.abs(rounded)\n    const direction = value >= 0 ? 'after' : 'before'\n\n    return `${to} is ${abs} ${unit} ${direction} ${from}`\n  },\n})\n\n/**\n * Convert total seconds into a human-readable duration string.\n *\n * @remarks\n * Examples: `3725` → `1 hour, 2 minutes and 5 seconds`. Negative inputs are prefixed with `-`.\n * Zero seconds returns the literal `0 seconds`.\n */\nexport const durationFormatTool = new Tool({\n  name: 'duration_format',\n  description:\n    'Convert a total number of seconds into a human-readable duration breakdown (e.g. \"2 hours, 15 minutes and 30 seconds\").',\n  inputSchema: validator.object({\n    seconds: validator.number().required().description('Total number of seconds (may be negative)'),\n  }),\n  handler: async (args) => {\n    const { seconds: totalSeconds } = args as { seconds: number }\n    const sign = totalSeconds < 0 ? '-' : ''\n    const absSeconds = Math.abs(totalSeconds)\n\n    const dur = Duration.fromObject({ seconds: absSeconds }).shiftTo(\n      'years',\n      'months',\n      'weeks',\n      'days',\n      'hours',\n      'minutes',\n      'seconds'\n    )\n    const obj = dur.toObject()\n\n    const units: Array<[keyof typeof obj, string, string]> = [\n      ['years', 'year', 'years'],\n      ['months', 'month', 'months'],\n      ['weeks', 'week', 'weeks'],\n      ['days', 'day', 'days'],\n      ['hours', 'hour', 'hours'],\n      ['minutes', 'minute', 'minutes'],\n      ['seconds', 'second', 'seconds'],\n    ]\n\n    const parts: string[] = []\n    for (const [key, singular, plural] of units) {\n      const v = Math.floor(obj[key] ?? 0)\n      if (v > 0) parts.push(`${v} ${v === 1 ? singular : plural}`)\n    }\n\n    if (parts.length === 0) return '0 seconds'\n    const formatted =\n      parts.length === 1\n        ? parts[0]\n        : parts.slice(0, -1).join(', ') + ' and ' + parts[parts.length - 1]\n    return `${sign}${formatted}`\n  },\n})\n"],"mappings":"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"}