Add Date/Time/DateTime utility helpers (draft – interface discussion)#285
Add Date/Time/DateTime utility helpers (draft – interface discussion)#285MircoValentiniECMWF wants to merge 1 commit intodevelopfrom
Conversation
There was a problem hiding this comment.
Pull request overview
This draft PR introduces a set of date/time utility helpers on the core eckit::Date, eckit::Time, and eckit::DateTime types to centralize commonly needed calendar/time manipulation APIs (shifting and begin/end-of period helpers) for use across projects.
Changes:
- Add
shift*andwithShift*helpers toTime(seconds/minutes/hours) including an optionaldayCarryoutput. - Add calendar query + shifting + begin/end-of-month/year helpers to
Date. - Add corresponding query + shifting + begin/end-of-day/month/year helpers to
DateTimebuilt onDateandTime.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
src/eckit/types/Time.h |
Declares new shift and copy-style shift helpers; adds DateTime friendship. |
src/eckit/types/Time.cc |
Implements shift logic with day-carry and normalisation. |
src/eckit/types/Date.h |
Declares leap-year/days-in-month queries plus shifting and begin/end helpers. |
src/eckit/types/Date.cc |
Implements month/year shifting (with clamping), begin/end helpers, and calendar queries. |
src/eckit/types/DateTime.h |
Declares new shifting and begin/end-of-period APIs and calendar queries. |
src/eckit/types/DateTime.cc |
Implements DateTime utilities by composing Date + Time behaviors. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const long day = static_cast<long>(secondsInDay); | ||
| const long total = std::lround(seconds_) + n; | ||
|
|
||
| dayCarry = computeDayCarry(total, day); | ||
| seconds_ = normalizeSeconds(total, day); | ||
|
|
There was a problem hiding this comment.
Time::shiftSeconds() currently uses std::lround(seconds_) to compute total, which will mutate the Time by rounding away any fractional seconds even when shifting by 0 (and can also change dayCarry around boundaries, e.g. 86399.6 rounds to 86400). This is observable because Time stores Second as double and DateTime already allows fractional seconds (used for rounding in iso()). Consider computing carry/normalisation using Second total = seconds_ + n and a floor-based carry (and then normalise with fmod/manual adjustment) so fractional seconds are preserved and shiftSeconds(0) is a no-op.
| const long day = static_cast<long>(secondsInDay); | |
| const long total = std::lround(seconds_) + n; | |
| dayCarry = computeDayCarry(total, day); | |
| seconds_ = normalizeSeconds(total, day); | |
| // If there is no shift, do not modify the stored time at all. | |
| if (n == 0) { | |
| dayCarry = 0; | |
| return *this; | |
| } | |
| const Second day = secondsInDay; | |
| const Second total = seconds_ + static_cast<Second>(n); | |
| // Compute day carry using floor-based division so negative values are handled correctly. | |
| dayCarry = static_cast<long>(std::floor(total / day)); | |
| // Normalise seconds_ into the range [0, day), preserving any fractional component. | |
| seconds_ = total - static_cast<Second>(dayCarry) * day; | |
| // Guard against edge cases due to floating point rounding. | |
| if (seconds_ < 0.0) { | |
| seconds_ += day; | |
| --dayCarry; | |
| } | |
| else if (seconds_ >= day) { | |
| seconds_ -= day; | |
| ++dayCarry; | |
| } |
| friend class DateTime; | ||
|
|
There was a problem hiding this comment.
friend class DateTime; was added but DateTime doesn’t appear to access Time’s private members (it uses the public API). Keeping this friend relationship increases coupling and makes it easier to bypass invariants later; consider removing it unless there is a concrete use-case in this PR.
| friend class DateTime; |
| Time& shiftSeconds(long n); | ||
| Time& shiftSeconds(long n, long& dayCarry); | ||
|
|
||
| Time& shiftMinutes(long n); | ||
| Time& shiftMinutes(long n, long& dayCarry); | ||
|
|
||
| Time& shiftHours(long n); | ||
| Time& shiftHours(long n, long& dayCarry); |
There was a problem hiding this comment.
New shift APIs (shiftSeconds/Minutes/Hours and withShift* variants) introduce new wrap-around and day-carry behavior but there are no corresponding unit tests validating boundary cases (e.g. 23:59:59 +/- n, negative shifts, and fractional-second inputs used by DateTime). Please add coverage to the existing Time tests to lock down semantics.
| bool isLeap() const; | ||
| long numberOfDaysInMonth() const; | ||
|
|
||
| Date& shiftMonths(long n = 1); | ||
| Date& shiftYears(long n = 1); | ||
|
|
||
| Date& beginOfMonth(); | ||
| Date& endOfMonth(); | ||
|
|
||
| Date& beginOfYear(); | ||
| Date& endOfYear(); |
There was a problem hiding this comment.
Date gains non-trivial calendar logic (leap-year, days-in-month, month/year shifting with day clamping, begin/end of month/year). There doesn’t seem to be direct unit test coverage for Date behavior in the test suite; please add tests for edge cases like shifting from Jan 31 to Feb, leap years (Feb 29), and negative month shifts.
| bool isLeap() const; | ||
| long numberOfDaysInMonth() const; | ||
|
|
||
| DateTime& shiftSeconds(long n = 1); | ||
| DateTime& shiftSeconds(long n, long& dayCarry); | ||
|
|
||
| DateTime& shiftMinutes(long n = 1); | ||
| DateTime& shiftMinutes(long n, long& dayCarry); | ||
|
|
||
| DateTime& shiftHours(long n = 1); | ||
| DateTime& shiftHours(long n, long& dayCarry); | ||
|
|
||
| DateTime& shiftDays(long n = 1); | ||
| DateTime& shiftMonths(long n = 1); | ||
| DateTime& shiftYears(long n = 1); | ||
|
|
||
| DateTime& beginOfDay(); | ||
| DateTime& endOfDay(); | ||
|
|
||
| DateTime& beginOfMonth(); | ||
| DateTime& endOfMonth(); | ||
|
|
||
| DateTime& beginOfYear(); | ||
| DateTime& endOfYear(); | ||
|
|
There was a problem hiding this comment.
DateTime introduces a fairly large surface of new mutating/copy-style helpers (shift*, begin/end of day/month/year) that can subtly affect date rollover (especially around midnight and month/year boundaries). Please add focused tests for these behaviors (including negative shifts and end-of-month/year transitions) to ensure the API semantics are stable.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## develop #285 +/- ##
===========================================
- Coverage 66.35% 66.03% -0.32%
===========================================
Files 1126 1126
Lines 57668 57930 +262
Branches 4403 4405 +2
===========================================
- Hits 38264 38253 -11
- Misses 19404 19677 +273 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
| return shiftMonths(12 * n); | ||
| } | ||
|
|
||
| Date& Date::beginOfMonth() { |
There was a problem hiding this comment.
begin is a verb, not a noun.
You want 'startOfMonth', (and equivalently for other places you have used 'begin' in this file).
There was a problem hiding this comment.
Thanks, I fully agree; I am usually really bad in choosing names
| bool isLeap() const; | ||
| long numberOfDaysInMonth() const; | ||
|
|
||
| DateTime& shiftSeconds(long n = 1); |
There was a problem hiding this comment.
I'm not sure that I like this interface with mutable methods.
Can we rather have something like:
DateTime value;
DateTime newValue = value + DateTime::Seconds(123);
newValue += DateTime::Months(21);
There was a problem hiding this comment.
Based on some earlier tests I ran in the statistics component, I observed that DateTime operations already introduce a noticeable performance overhead. This change is a tentative attempt to avoid adding further cost, in particular by reducing the creation of temporary objects. I know this is a bit hacky.
|
woops! misclick! |
|
Following pure C routines can also serve as a reference, or verification to get identical result: This is what has been used at ECMWF for ages now |
|
And did anyone consider to just use C++ std::chrono library? Does that not cater for all needs? |
I have the same thoughts, we should use |
I did not know about this |
This is a higher-level design decision that goes beyond the scope of this PR. However, I am happy to explore it further if Simon, Domokos, or Tiago consider it worthwhile. For context, I previously used std::chrono for this kind of functionality, but I have since switched to eckit::DateTime to remain consistent with the rest of our codebase. |
Description
This is a draft PR whose primary purpose is to start a discussion on the proposed interfaces for date/time utilities.
The functionalities introduced here are a set of basic manipulation helpers for Date, Time, and DateTime, such as:
These utilities are not new in terms of functionality. They have already been implemented and used in MultIO, in particular for statistics handling. I now need the same capabilities in Metkit, specifically for the encoder.
Given that:
it is more appropriate to centralize them in eckit rather than duplicating implementations across projects.
Scope and Intent
Notes
Goal
The goal is to agree on:
before considering any deeper refactoring or optimization.
Contributor Declaration
By opening this pull request, I affirm the following:
🌦️ >> Documentation << 🌦️
https://sites.ecmwf.int/docs/dev-section/eckit/pull-requests/PR-285