REMORA
Regional Modeling of Oceans Refined Adaptively
Loading...
Searching...
No Matches
REMORA_TimeUnits.H
Go to the documentation of this file.
1#ifndef REMORA_TimeUnits_H
2#define REMORA_TimeUnits_H
3
4#include <cctype>
5#include <cstdio>
6#include <sstream>
7#include <string>
8#include <vector>
9
10#include "REMORA_DateClock.H"
11
12/** \file REMORA_TimeUnits.H
13 *
14 * The CF `units` attribute of a NetCDF time variable, and how it maps onto the
15 * model clock.
16 *
17 * A forcing, boundary, river or climatology file stamps its records with a
18 * time variable whose `units` read `days since <date>`, or just `days`. The
19 * model clock counts from the date `remora.time_ref` names (see
20 * REMORA_DateClock.H), so a file time is on the model clock only after the
21 * difference between the two reference dates is added:
22 *
23 * model days = file days + (datenum(file reference) - datenum(time_ref))
24 *
25 * remora_time_units_offset_days computes that bracket, in the calendar
26 * `time_ref` selects. It is zero when the file counts from the same date as the
27 * run, which is the usual case, and a file with no reference date is taken to be
28 * on the model clock already.
29 *
30 * remora_parse_time_units reads the attribute. It accepts the CF form,
31 * `<unit> since YYYY-MM-DD[ hh:mm[:ss[.s]]][ UTC|Z|+00:00]`, and it skips words
32 * between `since` and the date, so `days since initialization, 2020-01-01
33 * 00:00:00` reads as 2020-01-01. It does not interpret a time zone other than
34 * UTC. Host only: it works on std::string.
35 */
36
37//! A parsed CF time-units attribute.
39{
40 //! The unit word, lower-cased: `days`, `seconds`, ...
41 std::string unit;
42 //! Whether a `since <date>` reference date followed the unit
43 bool has_reference = false;
44 int year = 0;
45 int month = 0;
46 int day = 0;
47 int hour = 0;
48 int minute = 0;
49 double second = 0.0;
50};
51
53
54inline std::string lower (std::string s)
55{
56 for (auto& c : s) {
57 // A NUL terminator stored with the attribute text would otherwise glue
58 // itself to the last word.
59 c = (c == '\0') ? ' ' : static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
60 }
61 return s;
62}
63
64//! `hh:mm[:ss[.s]]`, optionally suffixed `z`. Fills the time fields on success.
65inline bool parse_clock (const std::string& tok, REMORATimeUnits& out)
66{
67 int h = 0, m = 0, n = 0;
68 double s = 0.0;
69 bool matched = (std::sscanf(tok.c_str(), "%d:%d:%lf%n", &h, &m, &s, &n) == 3);
70 if (!matched) {
71 s = 0.0;
72 n = 0;
73 matched = (std::sscanf(tok.c_str(), "%d:%d%n", &h, &m, &n) == 2);
74 }
75 if (!matched) {
76 return false;
77 }
78 const std::string rest = tok.substr(n);
79 if (!rest.empty() && rest != "z") {
80 return false;
81 }
82 out.hour = h;
83 out.minute = m;
84 out.second = s;
85 return true;
86}
87
88//! `[-]YYYY-MM-DD`, optionally followed by `T` and a clock. Fills the date
89//! fields on success; a clock attached with `T` is parsed too.
90inline bool parse_date (const std::string& tok, REMORATimeUnits& out)
91{
92 int y = 0, mo = 0, d = 0, n = 0;
93 if (std::sscanf(tok.c_str(), "%d-%d-%d%n", &y, &mo, &d, &n) != 3) {
94 return false;
95 }
96 const std::string rest = tok.substr(n);
97 if (!rest.empty()) {
99 if (rest[0] != 't' || !parse_clock(rest.substr(1), clock)) {
100 return false;
101 }
102 out.hour = clock.hour;
103 out.minute = clock.minute;
104 out.second = clock.second;
105 }
106 out.year = y;
107 out.month = mo;
108 out.day = d;
109 return true;
110}
111
112//! A time-zone designator that means UTC
113inline bool is_utc (const std::string& tok)
114{
115 static const char* const names[] = {"utc", "gmt", "z", "+0", "-0", "+00", "-00",
116 "+0:00", "-0:00", "+00:00", "-00:00",
117 "+0000", "-0000"};
118 for (const char* name : names) {
119 if (tok == name) { return true; }
120 }
121 return false;
122}
123
124} // namespace remora_time_units_detail
125
126/**
127 * \brief Parse a CF time-units attribute.
128 *
129 * @param[in] units the attribute text, e.g. `days since 2020-01-01 00:00:00`
130 * @param[out] out the unit word and, if given, the reference date
131 * @param[out] error why parsing failed, when it did
132 * @return whether the attribute parsed. The unit word is not checked here; the
133 * caller decides which units it accepts.
134 */
135inline bool remora_parse_time_units (const std::string& units, REMORATimeUnits& out,
136 std::string& error)
137{
138 using namespace remora_time_units_detail;
139
141 error.clear();
142
143 std::vector<std::string> tok;
144 {
145 std::istringstream iss(lower(units));
146 std::string w;
147 while (iss >> w) { tok.push_back(w); }
148 }
149 if (tok.empty()) {
150 error = "the units attribute is empty";
151 return false;
152 }
153 out.unit = tok[0];
154
155 std::size_t i = 1;
156 while (i < tok.size() && tok[i] != "since") { ++i; }
157 if (i == tok.size()) {
158 return true; // a bare unit: no reference date
159 }
160
161 // The first word after "since" that reads as a date. Anything before it, such
162 // as "initialization,", is commentary.
163 for (++i; i < tok.size(); ++i) {
164 if (parse_date(tok[i], out)) { break; }
165 }
166 if (i == tok.size()) {
167 error = "no date of the form yyyy-mm-dd follows 'since'";
168 return false;
169 }
170 out.has_reference = true;
171 ++i;
172
173 if (i < tok.size() && parse_clock(tok[i], out)) { ++i; }
174 if (i < tok.size()) {
175 if (!is_utc(tok[i])) {
176 error = "'" + tok[i] + "' is not a time of day or UTC; reference dates in"
177 " another time zone are not supported";
178 return false;
179 }
180 ++i;
181 }
182 if (i < tok.size()) {
183 error = "unexpected '" + tok[i] + "' after the reference date";
184 return false;
185 }
186
187 if (out.month < 1 || out.month > 12 || out.day < 1 || out.day > 31 ||
188 out.hour < 0 || out.hour > 23 || out.minute < 0 || out.minute > 59 ||
189 out.second < 0.0 || out.second >= 61.0) {
190 error = "the reference date is out of range";
191 return false;
192 }
193 return true;
194}
195
196/**
197 * \brief Days to add to a time counted from \p units' reference date to count it
198 * from the date \p time_ref names instead.
199 *
200 * Zero for a file with no reference date, which is taken to be on the model
201 * clock already, and zero when the two dates agree. The day numbers are both
202 * taken in the calendar \p time_ref selects, as remora_caldate does.
203 */
204inline double remora_time_units_offset_days (const REMORATimeUnits& units, double time_ref)
205{
206 if (!units.has_reference) {
207 return 0.0;
208 }
209 return remora_datenum(time_ref, units.year, units.month, units.day,
210 units.hour, units.minute, units.second)
211 - remora_ref_datenum(time_ref);
212}
213
214/**
215 * \brief Whether a CF `calendar` attribute names the calendar \p time_ref selects.
216 *
217 * `360_day` goes with `time_ref = -1` and nothing else. Every other `time_ref`
218 * is Gregorian: `gregorian`, `standard` and `proleptic_gregorian` all agree for
219 * dates after 1582, which is where the model clock lives. `julian`, `noleap`
220 * and the rest count days differently and are rejected.
221 */
222inline bool remora_calendar_matches_time_ref (const std::string& calendar, double time_ref)
223{
224 const std::string c = remora_time_units_detail::lower(calendar);
225 // The attribute may carry a NUL terminator, which lower() turned into a space.
226 const std::string name = c.substr(0, c.find_last_not_of(' ') + 1);
227 if (static_cast<int>(time_ref) == -1) {
228 return name == "360_day";
229 }
230 return name == "gregorian" || name == "standard" || name == "proleptic_gregorian";
231}
232
233/** \brief The reference date of \p units as `YYYY-MM-DD hh:mm:ss`. */
235{
236 char buf[6 * 11 + 5 + 1];
237 std::snprintf(buf, sizeof(buf), "%04d-%02d-%02d %02d:%02d:%02d",
238 units.year, units.month, units.day, units.hour, units.minute,
239 static_cast<int>(units.second));
240 return std::string(buf);
241}
242
243#endif
AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE double remora_datenum(double time_ref, int year, int month, int day, int hour=0, int minutes=0, double seconds=0.0) noexcept
Fractional day number for a calendar date. ROMS datenum.
AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE double remora_ref_datenum(double time_ref) noexcept
Day number of the reference date implied by time_ref.
mf_h setVal(geomdata.ProbHi(2))
bool remora_calendar_matches_time_ref(const std::string &calendar, double time_ref)
Whether a CF calendar attribute names the calendar time_ref selects.
bool remora_parse_time_units(const std::string &units, REMORATimeUnits &out, std::string &error)
Parse a CF time-units attribute.
std::string remora_time_units_date_string(const REMORATimeUnits &units)
The reference date of units as YYYY-MM-DD hh:mm:ss.
double remora_time_units_offset_days(const REMORATimeUnits &units, double time_ref)
Days to add to a time counted from units' reference date to count it from the date time_ref names ins...
std::string lower(std::string s)
bool parse_clock(const std::string &tok, REMORATimeUnits &out)
hh:mm[:ss[.s]], optionally suffixed z. Fills the time fields on success.
bool is_utc(const std::string &tok)
A time-zone designator that means UTC.
bool parse_date(const std::string &tok, REMORATimeUnits &out)
A parsed CF time-units attribute.
std::string unit
The unit word, lower-cased: days, seconds, ...
bool has_reference
Whether a since <date> reference date followed the unit.