Paul King created GROOVY-12124:
----------------------------------
Summary: Modernize the TimeCategory date/time DSL: add a java.time
flavor and a dequirked java.util.Date flavor
Key: GROOVY-12124
URL: https://issues.apache.org/jira/browse/GROOVY-12124
Project: Groovy
Issue Type: Improvement
Reporter: Paul King
h1. Modernize the TimeCategory date/time DSL
h2. Summary
Introduce a {{java.time}}-based parallel to the legacy
{{groovy.time.TimeCategory}}
DSL, and give the existing {{java.util.Date}}-based DSL a cleaned-up
("dequirked")
home in the module that already owns Date support. Net result: one familiar DSL
({{1.hour.ago}}, {{date + 3.months}}, {{2.days.from.now}}) available in two
flavors —
modern {{java.time}} output or classic {{java.util.Date}} output — both
quirk-free.
h2. Background
{{groovy.time.TimeCategory}} (a {{use()}} category in *groovy core*) layers
duration
arithmetic onto {{java.util.Date}}/{{Calendar}}: {{Integer}} producers
({{1.hour}},
{{2.months}}), operators ({{date + duration}}, {{date - date}}), and
relative-time
properties ({{.ago}}, {{.from.now}}). It is backed by the
{{groovy.time.Duration}}
hierarchy (Duration / TimeDuration / DatumDependentDuration /
TimeDatumDependentDuration).
Two problems motivate this work:
* *No java.time equivalent.* Modern code uses {{java.time}}, but there is no
{{1.hour}}/{{2.months}} producer for it. The {{groovy-datetime}} module
already
provides all the arithmetic (plus/minus/next/multiply/between/upto…) on
{{java.time}}
types — the only gap is the number→amount producers and
{{.ago}}/{{.from.now}}.
* *The Date DSL is split and quirky.* All other {{java.util.Date}} DSL methods
({{date + int}}, {{date.next()}}, {{date[YEAR]}}, {{clearTime}}, {{format}},
{{date - date -> int}}) live in the optional *groovy-dateutil* module, while
TimeCategory lives in core — so half of Date arithmetic is core, half is
optional,
and {{date - date}} even resolves differently between them.
h2. Proposal
Add two categories and deprecate the legacy one:
|| Class || Module || Output type || Status ||
| {{org.apache.groovy.datetime.TimeCategory}} | groovy-datetime | {{java.time}}
(Duration/Period, LocalDate/LocalDateTime) | new |
| {{org.apache.groovy.dateutil.TimeCategory}} | groovy-dateutil |
{{java.util.Date}} | new (dequirked) |
| {{groovy.time.TimeCategory}} (+ Duration hierarchy) | core |
{{java.util.Date}} | deprecated, frozen |
The two new categories share one DSL surface and one semantics spec; only the
terminal
return types differ. The legacy class stays byte-for-byte behavior-compatible
(frozen),
so existing users are unaffected until they choose to migrate.
h3. Producer mapping (both flavors)
* {{seconds, minutes, hours, millis, nanos}} → {{java.time.Duration}}
(datetime) /
{{TimeDuration}} (dateutil)
* {{days, weeks, months, years}} → {{java.time.Period}} (datetime) /
{{Duration}}/{{DatumDependentDuration}} (dateutil)
* {{.ago}} / {{.from.now}} → {{LocalDate}}/{{LocalDateTime}} (datetime) /
{{java.util.Date}} (dateutil)
Note: {{java.time}} deliberately keeps date-based ({{Period}}) and time-based
({{Duration}}) amounts separate, so {{date + 2.months + 3.hours}} works by
left-associative chaining rather than a combined amount type.
h2. Design decisions
* *(proposed, pending team review)* The dateutil flavor uses its *own*
dequirked value
classes (option "B1"), not {{java.time}} internally (option "B2"). Rationale:
B1 keeps
the DSL return shape ({{.seconds}}/{{.days}} component accessors, bespoke
{{toString}},
the type lattice), so existing tests and user code port ~1:1; it is fully
self-contained
within groovy-dateutil (no dependency on groovy-datetime being on the
classpath — matching
today, where the Duration classes rely on no external extension methods). B2
would change
component-accessor and {{toString}} semantics and require either duplicating
datetime's
operator DGM or a new dateutil→datetime module dependency.
* *(proposed, pending team review)* Deprecate {{groovy.time.TimeCategory}} with
a removal
trajectory (future major). The dateutil copy gives Date users a
non-deprecated home, so
the deprecation is not coercive toward java.time.
* Keep the legacy {{groovy.time.Duration}} value classes exactly as-is (do not
move; the
dequirked classes are new copies in {{org.apache.groovy.dateutil}} — no split
package).
* The legacy class is *not* a forwarder to the new one (behavior differs); it
stays intact.
h2. Quirks removed in the new flavors
|| # || Legacy behavior || New behavior ||
| A | {{.ago}}/{{.from.now}} floor to midnight for day/month/year durations but
keep time for hours/min/sec (inconsistent) | Time-of-day preserved uniformly
(datetime: {{Period}}→{{LocalDate}}, {{Duration}}→{{LocalDateTime}}) |
| B | {{DatumDependentDuration.toMilliseconds()}} resolves against {{new
Date()}} — nondeterministic | Deterministic: uses {{ChronoUnit}} estimates
({{1.year == 12.months}} still holds exactly); {{java.time}} {{Period}} simply
has no {{toMillis}} |
| C | Four-class Duration lattice incl. {{TimeDatumDependentDuration}} |
Retained in dateutil (B1); not needed in datetime (chaining) |
| D | {{Duration}} = 24h/day for {{toMillis}} but DST-aware when added |
Documented/consistent |
| E | {{getTimeZone}} (already {{@Deprecated}}), {{getDaylightSavingsOffset}} |
Dropped from new flavors (superseded by zone-aware java.time) |
Side effect of A+B: {{getAgo}}/{{getFrom}}/{{toMilliseconds}} collapse to single
implementations on {{BaseDuration}}, removing the per-subclass overrides (~40%
less
duplication in the value classes).
h2. Prototype validation
A working B1 prototype of the dateutil flavor (six classes) was built and
exercised
against a port of {{TimeCategoryTest}}:
* All arithmetic/{{toString}}/comparison assertions ported with *only* the
{{use()}}/import
target changed — no assertion edits.
* Three added assertions demonstrate the dequirks; the same probes run against
the legacy
class confirm the delta (legacy {{3.days.ago}} → {{00:00:00}};
{{1.month.ago}} off by the
millis-since-midnight; {{5.months.toMilliseconds()}} varies with the current
date).
* Only the value-class suites ({{DurationTest}},
{{DatumDependentDurationTest}}) need edits,
confined to the ~6 methods that assert the midnight-flooring /
{{now}}-relative behavior.
h2. Scope / tasks
* [ ] {{org.apache.groovy.datetime.TimeCategory}} + tests + spec section
(_working-with-datetime-types_)
* [ ] {{org.apache.groovy.dateutil.TimeCategory}} (B1, dequirked) + value
classes + tests + spec section
* [ ] Deprecate {{groovy.time.TimeCategory}} (Javadoc {{@deprecated}} pointing
to both replacements)
* [ ] Reconcile/document the {{date - date}} collision (int days vs Duration)
within groovy-dateutil
* [ ] Add {{Long}}/{{nanos}} support (enhancement over the Integer-only legacy)
* [ ] Changelog + docgenerator entries
h2. Open questions (pending team review)
* B1 vs B2 for the dateutil flavor (recommendation: B1).
* Commit to eventual removal of {{groovy.time.TimeCategory}} (hard
{{@Deprecated}}), or
soft-deprecate in docs only?
* Category name: reuse the simple name {{TimeCategory}} in both new packages
(symmetry, but
dual-import hazard) vs a distinct name for the datetime flavor.
* Global DGM producers (static-compilation friendly) in addition to the
{{use()}} category —
now, later, or never?
--
This message was sent by Atlassian Jira
(v8.20.10#820010)