{"version":3,"file":"formatting.cjs","names":[],"sources":["../../../src/batteries/tools/formatting/index.ts"],"sourcesContent":["/**\n * Pre-constructed tools for locale-aware number, list, table, and text formatting.\n *\n * @module @nhtio/adk/batteries/tools/formatting\n *\n * @remarks\n * Pre-constructed bundled tools for the `formatting` category. Import individually, the whole\n * category, or import every tool via `@nhtio/adk/batteries`.\n */\n\nimport { Tool } from '@nhtio/adk/common'\nimport { isError } from '@nhtio/adk/guards'\nimport { validator } from '@nhtio/validation'\n\n/**\n * Format a number using locale-aware styles.\n *\n * @remarks\n * Supported styles: `decimal`, `currency`, `percent`, `compact` (e.g. `1.2K`), `scientific`\n * (e.g. `1.2e+3`), and `ordinal` (`1st`, `2nd`, `3rd`). Uses `Intl.NumberFormat` and\n * `Intl.PluralRules` from the JS standard library. Returns an error string for non-finite values\n * or invalid currency-without-currency-code.\n */\nexport const formatNumberTool = new Tool({\n  name: 'format_number',\n  description:\n    'Format a number using locale-aware styles: decimal, currency, percent, compact (1.2K), scientific (1.2e3), or ordinal (1st/2nd/3rd). Supports locale and precision options.',\n  inputSchema: validator.object({\n    value: validator.number().required().description('The number to format'),\n    style: validator\n      .string()\n      .valid('decimal', 'currency', 'percent', 'compact', 'scientific', 'ordinal')\n      .default('decimal')\n      .description('Formatting style (default: decimal)'),\n    currency: validator\n      .string()\n      .optional()\n      .allow('')\n      .description(\n        'ISO 4217 currency code — required when style is \"currency\" (e.g. \"USD\", \"EUR\"). An empty string is treated as not provided.'\n      ),\n    // eslint-disable-next-line adk/require-string-empty-disposition -- an empty locale is not a valid BCP 47 tag and should keep failing validation, unlike an omitted one which falls back to the default\n    locale: validator.string().default('en-US').description('BCP 47 locale tag (default: \"en-US\")'),\n    min_decimals: validator\n      .number()\n      .optional()\n      .description('Minimum fraction digits (default: style-dependent)'),\n    max_decimals: validator\n      .number()\n      .optional()\n      .description('Maximum fraction digits (default: style-dependent)'),\n  }),\n  handler: async (args) => {\n    const {\n      value,\n      style,\n      locale,\n      currency,\n      min_decimals: minDec,\n      max_decimals: maxDec,\n    } = args as {\n      value: number\n      style: string\n      locale: string\n      currency?: string\n      min_decimals?: number\n      max_decimals?: number\n    }\n\n    if (!Number.isFinite(value)) return `Error: Value must be a finite number (got ${value}).`\n\n    try {\n      if (style === 'ordinal') {\n        const pr = new Intl.PluralRules(locale, { type: 'ordinal' })\n        const suffixes: Record<string, string> = { one: 'st', two: 'nd', few: 'rd', other: 'th' }\n        const rule = pr.select(value)\n        const suffix = suffixes[rule] ?? 'th'\n        return `${new Intl.NumberFormat(locale).format(value)}${suffix}`\n      }\n\n      if (style === 'scientific') {\n        const exp = value === 0 ? 0 : Math.floor(Math.log10(Math.abs(value)))\n        const mantissa = value / Math.pow(10, exp)\n        const mFormatted = new Intl.NumberFormat(locale, {\n          minimumFractionDigits: minDec ?? 2,\n          maximumFractionDigits: maxDec ?? 6,\n        }).format(mantissa)\n        return `${mFormatted}e${exp >= 0 ? '+' : ''}${exp}`\n      }\n\n      const opts: Intl.NumberFormatOptions = {}\n\n      if (style === 'currency') {\n        if (!currency) return 'Error: \"currency\" parameter is required when style is \"currency\".'\n        opts.style = 'currency'\n        opts.currency = currency.toUpperCase()\n      } else if (style === 'percent') {\n        opts.style = 'percent'\n        opts.minimumFractionDigits = minDec ?? 1\n        opts.maximumFractionDigits = maxDec ?? 2\n      } else if (style === 'compact') {\n        opts.notation = 'compact'\n        opts.compactDisplay = 'short'\n      } else {\n        opts.style = 'decimal'\n      }\n\n      if (minDec !== undefined && style !== 'percent') opts.minimumFractionDigits = minDec\n      if (maxDec !== undefined && style !== 'percent') opts.maximumFractionDigits = maxDec\n\n      return new Intl.NumberFormat(locale, opts).format(value)\n    } catch (err) {\n      return `Error: ${isError(err) ? err.message : String(err)}`\n    }\n  },\n})\n\n/**\n * Format an array of items as a list.\n *\n * @remarks\n * Supported styles: `bullet` (`• item`), `numbered` (`1. item`), `inline_and`\n * (`a, b, and c`), `inline_or` (`a, b, or c`), `newline` (one per line).\n */\nexport const formatListTool = new Tool({\n  name: 'format_list',\n  description:\n    'Format an array of items as a list. Styles: bullet (• item), numbered (1. item), inline_and (\"a, b, and c\"), inline_or (\"a, b, or c\"), newline (one per line).',\n  inputSchema: validator.object({\n    items: validator\n      .array()\n      .items(validator.string())\n      .required()\n      .description('Array of items to format'),\n    style: validator\n      .string()\n      .valid('bullet', 'numbered', 'inline_and', 'inline_or', 'newline')\n      .default('bullet')\n      .description('List format style (default: bullet)'),\n    indent: validator.number().default(0).description('Spaces to indent each item (default: 0)'),\n  }),\n  handler: async (args) => {\n    const {\n      items,\n      style,\n      indent: rawIndent,\n    } = args as {\n      items: string[]\n      style: string\n      indent: number\n    }\n    // Clamp indent to a sane maximum: an unbounded value reaches `' '.repeat(indent)` and throws\n    // RangeError (Invalid string length). 100 spaces is far past any real formatting need.\n    const indent = Math.min(100, Math.max(0, Math.floor(rawIndent)))\n    const pad = ' '.repeat(indent)\n\n    if (items.length === 0) return ''\n\n    switch (style) {\n      case 'bullet':\n        return items.map((item) => `${pad}• ${item}`).join('\\n')\n      case 'numbered':\n        return items.map((item, i) => `${pad}${i + 1}. ${item}`).join('\\n')\n      case 'newline':\n        return items.map((item) => `${pad}${item}`).join('\\n')\n      case 'inline_and':\n      case 'inline_or': {\n        const conj = style === 'inline_and' ? 'and' : 'or'\n        if (items.length === 1) return items[0]\n        if (items.length === 2) return `${items[0]} ${conj} ${items[1]}`\n        const last = items[items.length - 1]\n        const rest = items.slice(0, -1)\n        return `${rest.join(', ')}, ${conj} ${last}`\n      }\n      default:\n        return `Error: Unknown style \"${style}\".`\n    }\n  },\n})\n"],"mappings":"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"}