Skip to content

feat: implement date/time formatting functions (issue #283)#659

Merged
david-waltermire merged 1 commit intometaschema-framework:developfrom
david-waltermire:feature/283-format-datetime
Feb 8, 2026
Merged

feat: implement date/time formatting functions (issue #283)#659
david-waltermire merged 1 commit intometaschema-framework:developfrom
david-waltermire:feature/283-format-datetime

Conversation

@david-waltermire
Copy link
Contributor

@david-waltermire david-waltermire commented Feb 7, 2026

Summary

  • Implement fn:format-integer (2-arg and 3-arg) per XPath Functions 3.1 section 4.6.1, supporting decimal digit patterns, Roman numerals, alphabetic, words, ordinal numbers, and grouping separators
  • Implement fn:format-dateTime, fn:format-date, fn:format-time (2-arg and 5-arg each) per XPath Functions 3.1 sections 9.8.1-9.8.3, with a shared picture string parser and formatting engine supporting all 16 component specifiers
  • Add FormatFunctionException (FODF1310) and FormatDateTimeFunctionException (FOFD1340, FOFD1350) error classes
  • Register all 8 new function signatures in DefaultFunctionLibrary
  • 204 unit tests covering exception classes, format-integer, picture string parsing, the formatting engine, and end-to-end function evaluation

Closes #283

Test plan

  • mvn -pl core test -Dtest="FormatFunctionExceptionTest,FormatDateTimeFunctionExceptionTest,FnFormatIntegerTest,DateTimeFormatUtilTest,FnFormatDateTimeTest,FnFormatDateTest,FnFormatTimeTest" passes (204 tests)
  • mvn clean install -PCI -Prelease passes all modules with 0 PMD violations, 0 SpotBugs bugs, 0 Checkstyle violations
  • Verify W3C spec examples from section 9.8.5 produce correct output (covered by FnFormatDateTest and FnFormatTimeTest)
  • Verify invalid picture strings raise FOFD1340 and invalid component specifiers raise FOFD1350

Summary by CodeRabbit

  • New Features

    • Added user-facing formatting functions: format-date, format-dateTime, format-time, and format-integer (multiple arities) with full XPath-style picture string support, locale/calendar/place options, width/modifier handling, timezone/era/fractional-second behavior.
  • Documentation

    • Added a detailed PRD and implementation plan covering goals, requirements, error codes, testing strategy, and acceptance criteria.
  • Tests

    • Added extensive unit and integration tests for parsing, formatting, locales, widths, modifiers, and error scenarios.
  • Chores

    • Recorded the initiative in Active PRDs.

@coderabbitai
Copy link

coderabbitai bot commented Feb 7, 2026

📝 Walkthrough

Walkthrough

Adds a new PRD and implementation for XPath‑style formatting: fn:format-integer, fn:format-dateTime, fn:format-date, fn:format-time; introduces DateTimeFormatUtil parser/formatter, new format exceptions, registers functions in DefaultFunctionLibrary, and adds extensive unit/integration tests covering parsing, formatting, modifiers, widths, locales, and error cases. (50 words)

Changes

Cohort / File(s) Summary
PRD tracking
\.claude/CLAUDE.md
Registers new Active PRD entry PRDs/20260207-format-datetime/ (FORMAT‑DT‑001, "In Progress").
Product docs
PRDs/20260207-format-datetime/PRD.md, PRDs/20260207-format-datetime/implementation-plan.md
Adds PRD and implementation plan with goals, architecture, APIs, error codes, phased plan, and test strategy.
Exceptions
core/src/main/java/.../function/FormatFunctionException.java, core/src/main/java/.../function/FormatDateTimeFunctionException.java
New exception classes with public error-code constants and constructors for format-related errors (FODF / FOFD prefixes).
Formatting core util
core/src/main/java/.../function/library/DateTimeFormatUtil.java
Large new utility: picture-string parser, component model (LiteralComponent, VariableMarkerComponent), formatDateTime entrypoint, marker/modifier/width handling, locale/calendar/place support, and validation/error reporting.
Format functions
core/src/main/java/.../function/library/FnFormatInteger.java, core/src/main/java/.../function/library/FnFormatDateTime.java, core/src/main/java/.../function/library/FnFormatDate.java, core/src/main/java/.../function/library/FnFormatTime.java
New public function classes exposing IFunction signatures (format-integer: 2/3-arg; format-dateTime/format-date/format-time: 2/5-arg). Handlers coerce inputs and delegate to DateTimeFormatUtil; include public formatting helpers.
Function registration
core/src/main/java/.../function/library/DefaultFunctionLibrary.java
Registers eight new function signatures for format-integer, format-dateTime, format-date, and format-time in the default function registry (replaces placeholder comments).
Tests
core/src/test/java/.../function/*Format*Test.java, core/src/test/java/.../function/Format*ExceptionTest.java, core/src/test/java/.../function/library/DateTimeFormatUtilTest.java
Adds extensive unit and integration tests for exceptions, DateTimeFormatUtil parsing/formatting, and each FnFormat* function, covering spec examples, markers/modifiers/widths, locales, timezone/era handling, and negative cases.

Sequence Diagram(s)

sequenceDiagram
    participant Caller as Caller (metapath)
    participant Lib as DefaultFunctionLibrary
    participant Fn as FnFormat* (Integer / Date / Time / DateTime)
    participant Util as DateTimeFormatUtil
    participant Temporal as ITemporalItem

    Caller->>Lib: resolve function
    Lib->>Fn: invoke handler(args)
    Fn->>Temporal: coerce/inspect value
    Fn->>Util: formatDateTime(value, picture, language, calendar, place, allowedMarkers)
    Util->>Util: parsePictureString(picture) / build components
    Util->>Temporal: extract components (year, month, hour, tz, digits...)
    Util->>Fn: return formatted string
    Fn->>Caller: return IStringItem
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • aj-stein

Poem

🐇
I hop through brackets, markers bright,
Stitching years and moonlit night.
Tokens tumble, widths align,
I bind the hours into line—
A rabbit’s stamp on formatted time.

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 53.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: implement date/time formatting functions (issue #283)' accurately and directly describes the main change: implementing date/time formatting functions per XPath 3.1 specifications.
Linked Issues check ✅ Passed The PR implements all three required functions (fn:format-dateTime, fn:format-date, fn:format-time) per XPath Functions 3.1 with both 2-arg and 5-arg signatures, registers them in DefaultFunctionLibrary, and includes comprehensive unit tests covering 204 test cases.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing date/time formatting functions: new exception classes, formatter utility, function implementations, registrations, and unit tests. No unrelated modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

No actionable comments were generated in the recent review. 🎉

🧹 Recent nitpick comments
core/src/main/java/dev/metaschema/core/metapath/function/library/DateTimeFormatUtil.java (1)

1050-1081: Consider edge case for military timezone letter mapping.

The military timezone logic for positive hours has a subtle issue in the comment vs. implementation:

  • Comment says "skip J at +10 position" but the actual skip is because J represents local time (no timezone), not a specific offset
  • The implementation correctly maps: A-I for +1 to +9, K-M for +10 to +12 (skipping J)

The code is correct, but the comment could be clearer.

📝 Suggested comment clarification
        if (hours <= 9) {
          return String.valueOf((char) ('A' + hours - 1));
        }
-        // hours 10,11,12: skip J so K=10, L=11, M=12
+        // hours 10,11,12: K=+10, L=+11, M=+12 (J is reserved for local/no timezone)
        return String.valueOf((char) ('A' + hours)); // +10->K, +11->L, +12->M

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 PMD (7.21.0)
core/src/main/java/dev/metaschema/core/metapath/function/library/FnFormatTime.java

[ERROR] Cannot load ruleset pmd/category/java/custom.xml: Cannot resolve rule/ruleset reference 'pmd/category/java/custom.xml'. Make sure the resource is a valid file or URL and is on the CLASSPATH. Use --debug (or a fine log level) to see the current classpath.
[WARN] Progressbar rendering conflicts with reporting to STDOUT. No progressbar will be shown. Try running with argument -r to output the report to a file instead.

core/src/main/java/dev/metaschema/core/metapath/function/FormatDateTimeFunctionException.java

[ERROR] Cannot load ruleset pmd/category/java/custom.xml: Cannot resolve rule/ruleset reference 'pmd/category/java/custom.xml'. Make sure the resource is a valid file or URL and is on the CLASSPATH. Use --debug (or a fine log level) to see the current classpath.
[WARN] Progressbar rendering conflicts with reporting to STDOUT. No progressbar will be shown. Try running with argument -r to output the report to a file instead.

core/src/main/java/dev/metaschema/core/metapath/function/FormatFunctionException.java

[ERROR] Cannot load ruleset pmd/category/java/custom.xml: Cannot resolve rule/ruleset reference 'pmd/category/java/custom.xml'. Make sure the resource is a valid file or URL and is on the CLASSPATH. Use --debug (or a fine log level) to see the current classpath.
[WARN] Progressbar rendering conflicts with reporting to STDOUT. No progressbar will be shown. Try running with argument -r to output the report to a file instead.

  • 12 others

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@PRDs/20260207-format-datetime/implementation-plan.md`:
- Line 199: The example date/weekday pair in the spec for the format string
"[FNn], [D1o] [MNn] [Y]" is incorrect; change the example "Tuesday, 5th March
2026" to a correct pairing such as "Thursday, 5th March 2026" (or substitute a
matching date like "Wednesday, 4th March 2026" or "Monday, 2nd March 2026") so
the weekday and date are consistent with the format example.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In
`@core/src/main/java/dev/metaschema/core/metapath/function/library/DateTimeFormatUtil.java`:
- Line 610: The current line in DateTimeFormatUtil uses int year =
Math.abs(value.getYear()), which can overflow for Integer.MIN_VALUE; change the
handling to compute an absolute year safely (e.g., use a long and take
Math.abs((long) value.getYear()) or use Math.absExact if on Java 15+) and update
any dependent variables/formatting logic that assume an int to use the safe long
value or handle the possible ArithmeticException from Math.absExact; ensure
references to year in methods inside DateTimeFormatUtil are adjusted to the new
type or guarded accordingly.
- Line 794: DateTimeFormatUtil uses LocalDate.of(...) which throws for year <=
0; update all places that create a LocalDate from value.getYear() (e.g., the
LocalDate.of(...) call used by formatDayOfYear, formatDayOfWeek,
formatWeekOfYear, formatWeekOfMonth) to use a proxy year >= 1 (e.g., int
proxyYear = Math.max(1, value.getYear())) when constructing LocalDate so
calculations succeed for year 0 and negative years while leaving formatYear
(which already handles negatives) unchanged.

In
`@core/src/main/java/dev/metaschema/core/metapath/function/library/FnFormatInteger.java`:
- Around line 676-704: formatLargeNumber currently only knows scales up to
"trillion" (scaleWords) so for groups with index i >= scaleWords.length it
silently omits the scale; update formatLargeNumber to handle larger magnitudes
by either (a) extending the scaleWords array with additional entries
("quadrillion", "quintillion", etc.) to cover expected ranges or (b) adding a
clear fallback when i >= scaleWords.length that appends a decimal/exponential
representation (or returns value.toString()) instead of omitting the scale;
locate the scaleWords array and the loop that appends scaleWords in
formatLargeNumber and implement one of these fixes (ensure
numberToWords(BigInteger.valueOf(group)) is still used for the group text and
the fallback triggers before finishing the result).
- Around line 41-88: The two-argument signature currently marks contextDependent
but executeTwoArg ignores the DynamicContext and passes null for language to
fnFormatInteger; modify executeTwoArg to call
dynamicContext.getStaticContext().getDefaultLanguage() and pass that value into
fnFormatInteger (same approach as FnDefaultLanguage), so the 2-arg form uses the
context's default language for language-dependent formatting.
🧹 Nitpick comments (4)
core/src/test/java/dev/metaschema/core/metapath/function/library/DateTimeFormatUtilTest.java (1)

919-931: Test assertion could be more specific.

The testWeekOfMonth test uses a loose assertion (>= 1 && <= 6) rather than verifying the exact expected value. For 2002-12-31, the week of month should be deterministic. Consider using assertEquals with the expected value for more precise validation.

💡 Proposed fix for more specific assertion
   `@Test`
   void testWeekOfMonth() {
-    // 2002-12-31 -> week of month varies; just check it's a valid number
     String result = format(TEST_DATE, "[w]", DATE_MARKERS);
-    assertTrue(Integer.parseInt(result) >= 1 && Integer.parseInt(result) <= 6);
+    // 2002-12-31 is in week 5 of December
+    assertEquals("5", result);
   }
core/src/main/java/dev/metaschema/core/metapath/function/library/FnFormatTime.java (1)

36-43: Clarify TIME_MARKERS inclusion of C and E in comment.

The Javadoc at lines 37-38 states the set "excludes date-only markers", but 'C' (calendar) and 'E' (era) are included. While this may be correct per the XPath 3.1 spec (as these can relate to time zone context), the comment could be misleading. Consider updating the comment to clarify why C and E are included for time values.

📝 Suggested documentation improvement
   /**
-   * The set of component specifiers allowed for time values, which excludes
-   * date-only markers (Y, M, D, d, F, W, w).
+   * The set of component specifiers allowed for time values. Excludes
+   * date-only markers (Y, M, D, d, F, W, w) but includes C (calendar) and
+   * E (era) which can apply to time values in certain contexts.
    */
core/src/main/java/dev/metaschema/core/metapath/function/library/DateTimeFormatUtil.java (2)

473-479: Unused parameters calendar and place.

The calendar and place parameters are declared but not used in the formatting logic. While this may be intentional for API compatibility with the XPath 5-argument signature (where behavior is implementation-defined), consider either:

  1. Adding a brief comment noting these are reserved for future implementation, or
  2. Logging a warning when non-null values are passed

916-921: Unused secondMod parameter in formatAmPm.

The secondMod parameter is declared and documented but never used in the implementation. Either remove it from the signature or add handling for relevant modifiers.

♻️ Remove unused parameter
   `@NonNull`
   private static String formatAmPm(
       int hour,
       `@Nullable` String primaryMod,
-      `@Nullable` Character secondMod,
       `@Nullable` Integer minWidth,
       `@Nullable` Integer maxWidth) {

Note: This would also require updating the call site at line 550.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@PRDs/20260207-format-datetime/implementation-plan.md`:
- Around line 417-465: Add an explicit acceptance checklist item under the
"Quality" (or "Acceptance Criteria") section to ensure website/README updates
are performed; specifically add a bullet like "Website/README updated to
document new fn:format-date/time/integer behavior and examples" so reviewers can
verify the documentation task is completed alongside code changes and tests.

In `@PRDs/20260207-format-datetime/PRD.md`:
- Around line 316-319: The phrase "single digit" in section 9.8.4.5 should be
hyphenated for grammar: change occurrences of "single digit format token" to
"single-digit format token" in the fractional seconds section (referencing
tokens [f1], [f001], [f01] and the section header 9.8.4.5) so the spec reads "A
single-digit format token (`[f1]`) retains all fractional digits" for clarity
and consistency.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In
`@core/src/main/java/dev/metaschema/core/metapath/function/library/FnFormatTime.java`:
- Around line 36-43: TIME_MARKERS in FnFormatTime currently includes the era
marker 'E', which is date-related and must be excluded; update the static final
Set<Character> TIME_MARKERS in FnFormatTime.java to remove 'E' so it only
contains 'H', 'h', 'P', 'm', 's', 'f', 'Z', 'z', 'C', ensuring fn:format-time
will correctly treat era markers as invalid for time values and trigger FOFD1350
where appropriate.
🧹 Nitpick comments (1)
core/src/main/java/dev/metaschema/core/metapath/function/library/FnFormatInteger.java (1)

656-663: Minor: Number-to-words format doesn't include "and" connector.

The implementation produces "one hundred twenty-three" while the implementation plan (line 116) shows "one hundred and twenty-three". Both are acceptable English forms, but ensure tests align with the actual output format. The current implementation (no "and") is consistent with American English convention.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In
`@core/src/main/java/dev/metaschema/core/metapath/function/library/FnFormatTime.java`:
- Around line 48-66: The implementations in FnFormatTime must return an empty
sequence (no item) when the input time argument is null/empty and must use the
dynamic context default language when the 5‑arg call receives an empty
$language; update executeTwoArg and executeFiveArg to check for a
missing/time-null value and return an empty sequence instead of a single
empty-string item, ensure executeFiveArg falls back to the dynamic context’s
default language when $language is empty, and update the function signatures
(e.g., SIGNATURE_TWO_ARG and the corresponding SIGNATURE_FIVE_ARG) to use the
correct return cardinality (zeroOrOne) matching the empty-sequence behavior.

In `@PRDs/20260207-format-datetime/PRD.md`:
- Around line 494-502: Update the Verification Checklist under the "### 5.4
Verification Checklist" section in PRD.md to include a new checkbox for the
website/README update required by the acceptance criteria; add a line like "- [
] Website and README updated to reflect changes" (or similar) so the checklist
explicitly tracks that task and ensure it appears alongside the other existing
checkboxes.

@david-waltermire david-waltermire force-pushed the feature/283-format-datetime branch from e074689 to 16def85 Compare February 8, 2026 02:54
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In
`@core/src/main/java/dev/metaschema/core/metapath/function/library/FnFormatDateTime.java`:
- Around line 63-178: The function signatures currently declare .returnOne() and
the handlers executeTwoArg and executeFiveArg always return a string item (using
formatDateTime even when value==null), but per spec a null/empty $value should
produce an empty sequence; change the signature builders that use executeTwoArg
and executeFiveArg to .returnZeroOrOne(), and update executeTwoArg and
executeFiveArg to return ISequence.empty() when the parsed IDateTimeItem value
is null (otherwise keep returning IStringItem.of(formatDateTime(...))). Ensure
formatDateTime remains unchanged.

In
`@core/src/main/java/dev/metaschema/core/metapath/function/library/FnFormatTime.java`:
- Around line 47-104: The function signatures SIGNATURE_TWO_ARG and
SIGNATURE_FIVE_ARG declare .returnOne() but must be .returnZeroOrOne() to match
xs:string? per XPath 3.1; update both builders to .returnZeroOrOne() and adjust
the handlers executeTwoArg and executeFiveArg to return ISequence.empty() when
the incoming "value" is null/empty instead of returning a single empty-string
item (do not wrap "" in a sequence). Ensure the change is applied to the builder
calls and the null/empty branch in both executeTwoArg and executeFiveArg so
empty sequence input yields an empty sequence.
🧹 Nitpick comments (1)
core/src/main/java/dev/metaschema/core/metapath/function/library/FnFormatTime.java (1)

128-150: Consider falling back to default language in 5-arg form when $language is empty.

The 5-arg handler passes null for language when the argument is absent (line 147). The 2-arg form falls back to dynamicContext.getStaticContext().getDefaultLanguage() (line 122). For consistency with XPath 3.1 behavior where absent/empty language uses the default from context, consider applying the same fallback in the 5-arg form:

-    return ISequence.of(IStringItem.valueOf(
-        formatTime(
-            value,
-            picture.asString(),
-            language == null ? null : language.asString(),
+    String lang = language == null
+        ? dynamicContext.getStaticContext().getDefaultLanguage()
+        : language.asString();
+    return ISequence.of(IStringItem.valueOf(
+        formatTime(
+            value,
+            picture.asString(),
+            lang,

Implement the XPath 3.1 date/time and integer formatting functions per
the W3C XPath Functions 3.1 specification (sections 4.6.1 and 9.8).

Functions added:
- fn:format-integer with 2-arg and 3-arg signatures supporting decimal
  digit patterns, Roman numerals (i/I), alphabetic (a/A), words (w/W/Ww),
  ordinal numbers, and grouping separators
- fn:format-dateTime with 2-arg and 5-arg signatures
- fn:format-date with 2-arg and 5-arg signatures
- fn:format-time with 2-arg and 5-arg signatures

Key capabilities:
- Picture string parser supporting all 16 component specifiers (Y, M, D,
  d, F, W, w, H, h, P, m, s, f, Z, z, C, E) with presentation modifiers
  and width modifiers
- Formatting engine with year modulo, fractional seconds, timezone
  (numeric, military, GMT prefix), named months/days, and ordinal support
- Proper marker validation per value type (FOFD1350 for invalid markers)
- Ordinal modifier falls back to cardinal for non-decimal format tokens
- Safe Math.abs handling for Integer.MIN_VALUE year values
- FormatFunctionException (FODF1310) and FormatDateTimeFunctionException
  (FOFD1340, FOFD1350) error classes

Includes PRD documentation and 206 unit tests.

Closes metaschema-framework#283
@david-waltermire david-waltermire force-pushed the feature/283-format-datetime branch from 16def85 to 6a2a840 Compare February 8, 2026 03:37
@david-waltermire david-waltermire linked an issue Feb 8, 2026 that may be closed by this pull request
6 tasks
@david-waltermire david-waltermire merged commit 671b7f4 into metaschema-framework:develop Feb 8, 2026
6 checks passed
@david-waltermire david-waltermire deleted the feature/283-format-datetime branch February 8, 2026 15:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support date, date/time, and time formatting functions

2 participants