Skip to content

Commit fc53608

Browse files
authored
Merge pull request #329 from xylar/omega/fix-time-step-hh-mm-ss
Add support for time intervals in HH:MM:SS and similar formats This merge adds support for time intervals in the following formats: - `DDDD_HH:MM:SS(.sss...)` (previously supported) - `HH:MM:SS(.sss...)` - `MM:SS(.sss...)` - `SS(.sss...)` This will make compatibility with Polaris and MPAS-Ocean simpler.
2 parents 1b5cc62 + c78debc commit fc53608

File tree

7 files changed

+411
-9
lines changed

7 files changed

+411
-9
lines changed

components/omega/doc/devGuide/TimeMgr.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,18 @@ Finally, a time interval can be defined as the time between two time instants:
135135
OMEGA::TimeInterval MyDeltaTime = MyTimeInstant2 - MyTimeInstant1;
136136
```
137137

138+
Time intervals can also be created from a formatted string (this is the form
139+
used when reading configuration options like `TimeStep` and `RunDuration`).
140+
The supported string formats are:
141+
142+
- `DDDD_HH:MM:SS(.sss...)`
143+
- `HH:MM:SS(.sss...)`
144+
- `MM:SS(.sss...)`
145+
- `SS(.sss...)`
146+
147+
Days, hours and minutes are optional but must be in order if included.
148+
Fractional seconds are optional.
149+
138150
### 5. Alarm
139151

140152
The Alarm class is designed to trigger events at specified times. Alarms can be

components/omega/doc/userGuide/Driver.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,15 @@ time management of the simulation.
1414
```
1515
The `StartTime` and `StopTime` should be formatted strings in the form
1616
`YYYY-MM-DD_HH:MM:SS.SSSS` and the `RunDuration` should be a formatted string in
17-
the form `DDDD_HH:MM:SS.SSSS` (the actual width of each unit can be arbitrary
18-
and the separators can be any single non-numeric character). Either the
17+
one of the following forms:
18+
19+
- `DDDD_HH:MM:SS(.sss...)`
20+
- `HH:MM:SS(.sss...)`
21+
- `MM:SS(.sss...)`
22+
- `SS(.sss...)`
23+
24+
The actual width of each unit can be arbitrary and the separators can be any
25+
single non-numeric character. Either the
1926
`StopTime` or `RunDuration` can be set to `none` in order to use the other to
2027
determine the duration of the run. If both are set and `StopTime - StartTime`
2128
is incosistent with `RunDuration`, then `RunDuration` is used for the

components/omega/doc/userGuide/TimeStepping.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,16 @@ The following time steppers are currently available:
3232
| RungeKutta4 | classic fourth-order four-stage Runge Kutta method |
3333
3434
The time step refers to the main model time step used to advance the solution
35-
forward. The format is in ``dddd_hh:mm:ss`` for days, hours, minutes and
36-
seconds.
35+
forward. The time step is specified as a formatted string and can be provided
36+
in any of the following forms:
37+
38+
- ``DDDD_HH:MM:SS(.sss...)``
39+
- ``HH:MM:SS(.sss...)``
40+
- ``MM:SS(.sss...)``
41+
- ``SS(.sss...)``
42+
43+
Days, hours and minutes are optional but must be in order if included.
44+
Fractional seconds are optional.
3745
3846
The StartTime refers to the starting time for the simulation. It is in the
3947
format ``yyyy-mm-day_hh:mm:ss`` for year, month, day, hour, minute, second.

components/omega/src/infra/TimeMgr.cpp

Lines changed: 123 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
#include "Logging.h"
3333

3434
#include <algorithm>
35+
#include <cctype>
3536
#include <cfloat>
3637
#include <climits>
3738
#include <cmath>
@@ -2090,7 +2091,8 @@ TimeInterval::TimeInterval(
20902091
// Construct a time interval from a standard string in the form
20912092
// DDDD_HH:MM:SS.SSSS where the width of DD and SS strings can be of
20922093
// arbitrary width (within reason) and the separators can be any single
2093-
// non-numeric character
2094+
// non-numeric character. DD, HH, MM are optional but must be in order if
2095+
// included. Fractional seconds are optional.
20942096
TimeInterval::TimeInterval(
20952097
std::string &TimeString // [in] string form of time interval
20962098
) {
@@ -2099,15 +2101,131 @@ TimeInterval::TimeInterval(
20992101
IsCalendar = false;
21002102
CalInterval = 0;
21012103

2102-
// Extract variables from string
2104+
// Extract variables from string.
2105+
// Supported formats:
2106+
// - DDDD_HH:MM:SS(.sss...)
2107+
// - HH:MM:SS(.sss...)
2108+
// - MM:SS(.sss...)
2109+
// - SS(.sss...)
2110+
// Separators between fields may be any single non-numeric character.
21032111
I8 Day = 0;
21042112
I8 Hour = 0;
21052113
I8 Minute = 0;
21062114
R8 RSecond = 0.;
21072115

2108-
std::istringstream ss(TimeString);
2109-
char discard;
2110-
ss >> Day >> discard >> Hour >> discard >> Minute >> discard >> RSecond;
2116+
// Parse from the right so that '.' is always interpreted as the decimal
2117+
// point in the seconds field, while still allowing '.' to act as a
2118+
// separator between integer fields in the legacy format.
2119+
auto isSpace = [](char c) {
2120+
return std::isspace(static_cast<unsigned char>(c)) != 0;
2121+
};
2122+
auto isDigit = [](char c) {
2123+
return std::isdigit(static_cast<unsigned char>(c)) != 0;
2124+
};
2125+
2126+
const std::string &s = TimeString;
2127+
if (s.empty()) {
2128+
ABORT_ERROR("TimeMgr: empty time interval string");
2129+
}
2130+
2131+
std::size_t right = s.size();
2132+
while (right > 0 && isSpace(s[right - 1])) {
2133+
--right;
2134+
}
2135+
if (right == 0) {
2136+
ABORT_ERROR("TimeMgr: blank time interval string");
2137+
}
2138+
2139+
// Parse final seconds token (may include fractional part).
2140+
std::size_t secEnd = right;
2141+
std::size_t secBeg = secEnd;
2142+
while (secBeg > 0) {
2143+
char c = s[secBeg - 1];
2144+
if (isDigit(c) || c == '.') {
2145+
--secBeg;
2146+
} else if (isSpace(c)) {
2147+
// allow trailing whitespace only (already trimmed)
2148+
break;
2149+
} else {
2150+
break;
2151+
}
2152+
}
2153+
if (secBeg == secEnd) {
2154+
ABORT_ERROR("TimeMgr: invalid time interval string '{}'", TimeString);
2155+
}
2156+
2157+
try {
2158+
RSecond = std::stod(s.substr(secBeg, secEnd - secBeg));
2159+
} catch (...) {
2160+
ABORT_ERROR("TimeMgr: invalid seconds field in time interval string '{}'",
2161+
TimeString);
2162+
}
2163+
2164+
// Walk left parsing up to 3 integer fields (minute, hour, day).
2165+
std::size_t idx = secBeg;
2166+
auto parsePrevInt = [&](I8 &outVal) -> bool {
2167+
// Skip whitespace.
2168+
while (idx > 0 && isSpace(s[idx - 1])) {
2169+
--idx;
2170+
}
2171+
if (idx == 0) {
2172+
return false;
2173+
}
2174+
2175+
// Skip one or more non-digit separator characters.
2176+
bool sawSep = false;
2177+
while (idx > 0 && !isDigit(s[idx - 1]) && !isSpace(s[idx - 1])) {
2178+
sawSep = true;
2179+
--idx;
2180+
}
2181+
while (idx > 0 && isSpace(s[idx - 1])) {
2182+
--idx;
2183+
}
2184+
if (!sawSep) {
2185+
return false;
2186+
}
2187+
if (idx == 0 || !isDigit(s[idx - 1])) {
2188+
ABORT_ERROR("TimeMgr: invalid time interval string '{}'", TimeString);
2189+
}
2190+
2191+
std::size_t end = idx;
2192+
std::size_t beg = end;
2193+
while (beg > 0 && isDigit(s[beg - 1])) {
2194+
--beg;
2195+
}
2196+
try {
2197+
outVal = static_cast<I8>(std::stoll(s.substr(beg, end - beg)));
2198+
} catch (...) {
2199+
ABORT_ERROR(
2200+
"TimeMgr: invalid integer field in time interval string '{}'",
2201+
TimeString);
2202+
}
2203+
idx = beg;
2204+
return true;
2205+
};
2206+
2207+
I8 tmp = 0;
2208+
int nInts = 0;
2209+
if (parsePrevInt(tmp)) {
2210+
Minute = tmp;
2211+
++nInts;
2212+
}
2213+
if (parsePrevInt(tmp)) {
2214+
Hour = tmp;
2215+
++nInts;
2216+
}
2217+
if (parsePrevInt(tmp)) {
2218+
Day = tmp;
2219+
++nInts;
2220+
}
2221+
2222+
// Anything left besides whitespace is invalid.
2223+
while (idx > 0 && isSpace(s[idx - 1])) {
2224+
--idx;
2225+
}
2226+
if (idx != 0) {
2227+
ABORT_ERROR("TimeMgr: invalid time interval string '{}'", TimeString);
2228+
}
21112229

21122230
R8 SecondsSet = Day * SECONDS_PER_DAY + Hour * SECONDS_PER_HOUR +
21132231
Minute * SECONDS_PER_MINUTE + RSecond;

components/omega/test/CMakeLists.txt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,24 @@ add_omega_test(
368368
"-n;1"
369369
)
370370

371+
############################
372+
# TimeInterval parsing tests
373+
############################
374+
375+
add_omega_test(
376+
TIMEINTERVAL_PARSE_TEST
377+
testTimeIntervalParse.exe
378+
infra/TimeIntervalParseTest.cpp
379+
"-n;1"
380+
)
381+
382+
add_omega_test(
383+
TIMEINTERVAL_PARSE_EXTENDED_FORMATS_TEST
384+
testTimeIntervalParseExtendedFormats.exe
385+
infra/TimeIntervalParseExtendedFormatsTest.cpp
386+
"-n;1"
387+
)
388+
371389
##################
372390
# Reductions test
373391
##################
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
//===-- Test driver for OMEGA TimeInterval extended string formats -*- C++
2+
//-*-===/
3+
//
4+
// This test encodes the *desired* behavior for parsing time interval strings
5+
// from config (e.g., TimeIntegration::TimeStep and RunDuration).
6+
//
7+
// It intentionally checks "short" forms like HH:MM:SS(.sss), MM:SS(.sss), and
8+
// SS(.sss). Today, the implementation assumes the string always begins with
9+
// days (DDDD_HH:MM:SS(.sss)), so these cases reproduce the bug.
10+
//
11+
// This test is expected to fail until parsing is improved.
12+
//
13+
//===----------------------------------------------------------------------===/
14+
15+
#include "DataTypes.h"
16+
#include "Error.h"
17+
#include "Logging.h"
18+
#include "MachEnv.h"
19+
#include "TimeMgr.h"
20+
#include "mpi.h"
21+
22+
#include <cmath>
23+
#include <string>
24+
25+
using namespace OMEGA;
26+
27+
namespace {
28+
29+
struct ExpectedParts {
30+
I8 days{0};
31+
I8 hours{0};
32+
I8 minutes{0};
33+
I8 secondsWhole{0};
34+
R8 secondsFrac{0.0};
35+
};
36+
37+
bool nearlyEqual(R8 a, R8 b, R8 tol) { return std::fabs(a - b) <= tol; }
38+
39+
int checkSeconds(const std::string &label, const std::string &input,
40+
const ExpectedParts &exp, R8 tol = 1e-12) {
41+
42+
const R8 expectedSeconds =
43+
static_cast<R8>(exp.days) * static_cast<R8>(SECONDS_PER_DAY) +
44+
static_cast<R8>(exp.hours) * static_cast<R8>(SECONDS_PER_HOUR) +
45+
static_cast<R8>(exp.minutes) * static_cast<R8>(SECONDS_PER_MINUTE) +
46+
static_cast<R8>(exp.secondsWhole) + exp.secondsFrac;
47+
48+
std::string s = input; // ctor takes non-const ref
49+
TimeInterval interval(s);
50+
51+
R8 seconds{0.0};
52+
interval.get(seconds, TimeUnits::Seconds);
53+
54+
if (!nearlyEqual(seconds, expectedSeconds, tol)) {
55+
LOG_ERROR("{}: '{}' parsed seconds mismatch: got {}, expected {}", label,
56+
input, seconds, expectedSeconds);
57+
return 1;
58+
}
59+
60+
return 0;
61+
}
62+
63+
} // namespace
64+
65+
int main(int argc, char **argv) {
66+
67+
MPI_Init(&argc, &argv);
68+
69+
MachEnv::init(MPI_COMM_WORLD);
70+
MachEnv *defEnv = MachEnv::getDefault();
71+
initLogging(defEnv);
72+
73+
int errCount = 0;
74+
75+
// Desired: HH:MM:SS(.sss)
76+
errCount += checkSeconds("Extended TimeInterval parse", "01:23:45",
77+
ExpectedParts{0, 1, 23, 45, 0.0});
78+
errCount += checkSeconds("Extended TimeInterval parse", "01:23:45.678",
79+
ExpectedParts{0, 1, 23, 45, 0.678});
80+
81+
// Desired: MM:SS(.sss)
82+
errCount += checkSeconds("Extended TimeInterval parse", "23:45.678",
83+
ExpectedParts{0, 0, 23, 45, 0.678});
84+
85+
// Desired: SS(.sss)
86+
errCount += checkSeconds("Extended TimeInterval parse", "45.678",
87+
ExpectedParts{0, 0, 0, 45, 0.678});
88+
errCount += checkSeconds("Extended TimeInterval parse", "45.6",
89+
ExpectedParts{0, 0, 0, 45, 0.6});
90+
errCount += checkSeconds("Extended TimeInterval parse", "45.000001",
91+
ExpectedParts{0, 0, 0, 45, 0.000001});
92+
93+
if (errCount != 0) {
94+
LOG_ERROR("TimeIntervalParseExtendedFormatsTest: FAIL ({} errors)",
95+
errCount);
96+
MPI_Finalize();
97+
return 1;
98+
}
99+
100+
LOG_INFO("TimeIntervalParseExtendedFormatsTest: PASS");
101+
102+
MPI_Finalize();
103+
return 0;
104+
}

0 commit comments

Comments
 (0)