752 lines
25 KiB
C++
752 lines
25 KiB
C++
#include "utils/temporal.hpp"
|
|
|
|
#include <charconv>
|
|
#include <chrono>
|
|
#include <ctime>
|
|
#include <string_view>
|
|
|
|
#include "utils/exceptions.hpp"
|
|
#include "utils/fnv.hpp"
|
|
|
|
namespace utils {
|
|
namespace {
|
|
|
|
constexpr bool IsInBounds(const auto low, const auto high, const auto value) { return low <= value && value <= high; }
|
|
|
|
constexpr bool IsValidDay(const uint8_t day, const uint8_t month, const uint16_t year) {
|
|
return std::chrono::year_month_day(std::chrono::year{year}, std::chrono::month{month}, std::chrono::day{day}).ok();
|
|
}
|
|
|
|
template <typename T>
|
|
std::optional<T> ParseNumber(const std::string_view string, const size_t size) {
|
|
if (string.size() < size) {
|
|
return std::nullopt;
|
|
}
|
|
|
|
T value{};
|
|
if (const auto [p, ec] = std::from_chars(string.data(), string.data() + size, value);
|
|
ec != std::errc() || p != string.data() + size) {
|
|
return std::nullopt;
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
Date::Date(const int64_t microseconds) {
|
|
namespace chrono = std::chrono;
|
|
const auto chrono_micros = chrono::microseconds(microseconds);
|
|
const auto s_days = chrono::sys_days(chrono::duration_cast<chrono::days>(chrono_micros));
|
|
const auto date = chrono::year_month_day(s_days);
|
|
years = static_cast<int>(date.year());
|
|
months = static_cast<unsigned>(date.month());
|
|
days = static_cast<unsigned>(date.day());
|
|
}
|
|
|
|
Date::Date(const DateParameters &date_parameters) {
|
|
if (!IsInBounds(0, 9999, date_parameters.years)) {
|
|
throw utils::BasicException(
|
|
"Creating a Date with invalid year parameter. The value should be an integer between 0 and 9999.");
|
|
}
|
|
|
|
if (!IsInBounds(1, 12, date_parameters.months)) {
|
|
throw utils::BasicException(
|
|
"Creating a Date with invalid month parameter. The value should be an integer between 1 and 12.");
|
|
}
|
|
|
|
if (!IsInBounds(1, 31, date_parameters.days) ||
|
|
!IsValidDay(date_parameters.days, date_parameters.months, date_parameters.years)) {
|
|
throw utils::BasicException(
|
|
"Creating a Date with invalid day parameter. The value should be an integer between 1 and 31, depending on the "
|
|
"month and year.");
|
|
}
|
|
|
|
years = date_parameters.years;
|
|
months = date_parameters.months;
|
|
days = date_parameters.days;
|
|
}
|
|
|
|
namespace {
|
|
tm GetUtcFromSystemClockOrThrow() {
|
|
namespace chrono = std::chrono;
|
|
const auto today = chrono::system_clock::to_time_t(chrono::system_clock::now());
|
|
tm utc_today;
|
|
if (!gmtime_r(&today, &utc_today)) {
|
|
throw utils::BasicException("Can't access clock's UTC time");
|
|
}
|
|
return utc_today;
|
|
}
|
|
|
|
int64_t TMYearToUtcYear(int year) { return year + 1900; }
|
|
|
|
int64_t TMMonthToUtcMonth(int month) { return month + 1; }
|
|
} // namespace
|
|
|
|
Date UtcToday() {
|
|
const auto utc_today = GetUtcFromSystemClockOrThrow();
|
|
return Date({TMYearToUtcYear(utc_today.tm_year), TMMonthToUtcMonth(utc_today.tm_mon), utc_today.tm_mday});
|
|
}
|
|
|
|
LocalTime UtcLocalTime() {
|
|
const auto utc_today = GetUtcFromSystemClockOrThrow();
|
|
return LocalTime({utc_today.tm_hour, utc_today.tm_min, utc_today.tm_sec});
|
|
}
|
|
|
|
LocalDateTime UtcLocalDateTime() {
|
|
const auto utc_today = GetUtcFromSystemClockOrThrow();
|
|
return LocalDateTime({TMYearToUtcYear(utc_today.tm_year), TMMonthToUtcMonth(utc_today.tm_mon), utc_today.tm_mday},
|
|
{utc_today.tm_hour, utc_today.tm_min, utc_today.tm_sec});
|
|
}
|
|
|
|
namespace {
|
|
constexpr auto *kSupportedDateFormatsHelpMessage = R"help(
|
|
String representing the date should be in one of the following formats:
|
|
|
|
- YYYY-MM-DD
|
|
- YYYYMMDD
|
|
- YYYY-MM
|
|
|
|
Symbol table:
|
|
|---|-------|
|
|
| Y | YEAR |
|
|
|---|-------|
|
|
| M | MONTH |
|
|
|---|-------|
|
|
| D | DAY |
|
|
|---|-------|)help";
|
|
|
|
} // namespace
|
|
|
|
std::pair<DateParameters, bool> ParseDateParameters(std::string_view date_string) {
|
|
// https://en.wikipedia.org/wiki/ISO_8601#Dates
|
|
// Date string with the '-' as separator are in the EXTENDED format,
|
|
// otherwise they are in a BASIC format
|
|
constexpr std::array valid_sizes{
|
|
10, // YYYY-MM-DD
|
|
8, // YYYYMMDD
|
|
7 // YYYY-MM
|
|
};
|
|
|
|
if (!std::any_of(
|
|
valid_sizes.begin(), valid_sizes.end(),
|
|
[date_string_size = date_string.size()](const auto valid_size) { return valid_size == date_string_size; })) {
|
|
throw utils::BasicException("Invalid string for date. {}", kSupportedDateFormatsHelpMessage);
|
|
}
|
|
|
|
DateParameters date_parameters;
|
|
auto maybe_year = ParseNumber<int64_t>(date_string, 4);
|
|
if (!maybe_year) {
|
|
throw utils::BasicException("Invalid year in the string. {}", kSupportedDateFormatsHelpMessage);
|
|
}
|
|
date_parameters.years = *maybe_year;
|
|
date_string.remove_prefix(4);
|
|
|
|
bool is_extended_format = false;
|
|
if (date_string.front() == '-') {
|
|
is_extended_format = true;
|
|
date_string.remove_prefix(1);
|
|
}
|
|
|
|
auto maybe_month = ParseNumber<int64_t>(date_string, 2);
|
|
if (!maybe_month) {
|
|
throw utils::BasicException("Invalid month in the string. {}", kSupportedDateFormatsHelpMessage);
|
|
}
|
|
date_parameters.months = *maybe_month;
|
|
date_string.remove_prefix(2);
|
|
|
|
if (!date_string.empty()) {
|
|
if (date_string.front() == '-') {
|
|
if (!is_extended_format) {
|
|
throw utils::BasicException("Invalid format for the date. {}", kSupportedDateFormatsHelpMessage);
|
|
}
|
|
date_string.remove_prefix(1);
|
|
}
|
|
|
|
auto maybe_day = ParseNumber<int64_t>(date_string, 2);
|
|
if (!maybe_day) {
|
|
throw utils::BasicException("Invalid month in the string. {}", kSupportedDateFormatsHelpMessage);
|
|
}
|
|
date_parameters.days = *maybe_day;
|
|
date_string.remove_prefix(2);
|
|
}
|
|
|
|
if (!date_string.empty()) {
|
|
throw utils::BasicException("Invalid format for the date. {}", kSupportedDateFormatsHelpMessage);
|
|
}
|
|
|
|
return {date_parameters, is_extended_format};
|
|
}
|
|
|
|
int64_t Date::MicrosecondsSinceEpoch() const {
|
|
namespace chrono = std::chrono;
|
|
return chrono::duration_cast<chrono::microseconds>(utils::DaysSinceEpoch(years, months, days)).count();
|
|
}
|
|
|
|
int64_t Date::DaysSinceEpoch() const { return utils::DaysSinceEpoch(years, months, days).count(); }
|
|
|
|
size_t DateHash::operator()(const Date &date) const {
|
|
utils::HashCombine<uint64_t, uint64_t> hasher;
|
|
size_t result = hasher(0, date.years);
|
|
result = hasher(result, date.months);
|
|
result = hasher(result, date.days);
|
|
return result;
|
|
}
|
|
|
|
namespace {
|
|
constexpr auto *kSupportedTimeFormatsHelpMessage = R"help(
|
|
String representing the time should be in one of the following formats:
|
|
|
|
- [T]hh:mm:ss
|
|
- [T]hh:mm
|
|
|
|
or
|
|
|
|
- [T]hhmmss
|
|
- [T]hhmm
|
|
- [T]hh
|
|
|
|
Symbol table:
|
|
|---|---------|
|
|
| h | HOURS |
|
|
|---|---------|
|
|
| m | MINUTES |
|
|
|---|---------|
|
|
| s | SECONDS |
|
|
|---|---------|
|
|
|
|
Additionally, seconds can be defined as decimal fractions with 3 or 6 digits after the decimal point.
|
|
First 3 digits represent milliseconds, while the second 3 digits represent microseconds.)help";
|
|
|
|
} // namespace
|
|
|
|
std::pair<LocalTimeParameters, bool> ParseLocalTimeParameters(std::string_view local_time_string) {
|
|
// https://en.wikipedia.org/wiki/ISO_8601#Times
|
|
// supported formats:
|
|
// hh:mm:ss.ssssss | hhmmss.ssssss
|
|
// hh:mm:ss.sss | hhmmss.sss
|
|
// hh:mm | hhmm
|
|
// hh
|
|
// Times without the separator are in BASIC format
|
|
// Times with ':' as a separator are in EXTENDED format.
|
|
if (local_time_string.front() == 'T') {
|
|
local_time_string.remove_prefix(1);
|
|
}
|
|
|
|
std::optional<bool> using_colon;
|
|
const auto process_optional_colon = [&] {
|
|
const bool has_colon = local_time_string.front() == ':';
|
|
if (!using_colon.has_value()) {
|
|
using_colon.emplace(has_colon);
|
|
}
|
|
|
|
if (*using_colon ^ has_colon) {
|
|
throw utils::BasicException(
|
|
"Invalid format for the local time. A separator should be used consistently or not at all. {}",
|
|
kSupportedTimeFormatsHelpMessage);
|
|
}
|
|
|
|
if (has_colon) {
|
|
local_time_string.remove_prefix(1);
|
|
}
|
|
|
|
if (local_time_string.empty()) {
|
|
throw utils::BasicException("Invalid format for the local time. {}", kSupportedTimeFormatsHelpMessage);
|
|
}
|
|
};
|
|
|
|
LocalTimeParameters local_time_parameters;
|
|
|
|
const auto maybe_hour = ParseNumber<int64_t>(local_time_string, 2);
|
|
if (!maybe_hour) {
|
|
throw utils::BasicException("Invalid hour in the string. {}", kSupportedTimeFormatsHelpMessage);
|
|
}
|
|
local_time_parameters.hours = *maybe_hour;
|
|
local_time_string.remove_prefix(2);
|
|
|
|
if (local_time_string.empty()) {
|
|
return {local_time_parameters, false};
|
|
}
|
|
|
|
process_optional_colon();
|
|
|
|
const auto maybe_minute = ParseNumber<int64_t>(local_time_string, 2);
|
|
if (!maybe_minute) {
|
|
throw utils::BasicException("Invalid minutes in the string. {}", kSupportedTimeFormatsHelpMessage);
|
|
}
|
|
local_time_parameters.minutes = *maybe_minute;
|
|
local_time_string.remove_prefix(2);
|
|
|
|
if (local_time_string.empty()) {
|
|
return {local_time_parameters, *using_colon};
|
|
}
|
|
|
|
process_optional_colon();
|
|
|
|
const auto maybe_seconds = ParseNumber<int64_t>(local_time_string, 2);
|
|
if (!maybe_seconds) {
|
|
throw utils::BasicException("Invalid seconds in the string. {}", kSupportedTimeFormatsHelpMessage);
|
|
}
|
|
local_time_parameters.seconds = *maybe_seconds;
|
|
local_time_string.remove_prefix(2);
|
|
|
|
if (local_time_string.empty()) {
|
|
return {local_time_parameters, *using_colon};
|
|
}
|
|
|
|
if (local_time_string.front() != '.') {
|
|
throw utils::BasicException("Invalid format for local time. {}", kSupportedTimeFormatsHelpMessage);
|
|
}
|
|
local_time_string.remove_prefix(1);
|
|
|
|
const auto maybe_milliseconds = ParseNumber<int64_t>(local_time_string, 3);
|
|
if (!maybe_milliseconds) {
|
|
throw utils::BasicException("Invalid milliseconds in the string. {}", kSupportedTimeFormatsHelpMessage);
|
|
}
|
|
local_time_parameters.milliseconds = *maybe_milliseconds;
|
|
local_time_string.remove_prefix(3);
|
|
|
|
if (local_time_string.empty()) {
|
|
return {local_time_parameters, *using_colon};
|
|
}
|
|
|
|
const auto maybe_microseconds = ParseNumber<int64_t>(local_time_string, 3);
|
|
if (!maybe_microseconds) {
|
|
throw utils::BasicException("Invalid microseconds in the string. {}", kSupportedTimeFormatsHelpMessage);
|
|
}
|
|
local_time_parameters.microseconds = *maybe_microseconds;
|
|
local_time_string.remove_prefix(3);
|
|
|
|
if (!local_time_string.empty()) {
|
|
throw utils::BasicException("Extra characters present at the end of the string.");
|
|
}
|
|
|
|
return {local_time_parameters, *using_colon};
|
|
}
|
|
|
|
LocalTime::LocalTime(const int64_t microseconds) {
|
|
auto chrono_microseconds = std::chrono::microseconds(microseconds);
|
|
if (chrono_microseconds.count() < 0) {
|
|
throw utils::BasicException("Negative LocalTime specified in microseconds");
|
|
}
|
|
|
|
const auto parsed_hours = GetAndSubtractDuration<std::chrono::hours>(chrono_microseconds);
|
|
if (parsed_hours > 23) {
|
|
throw utils::BasicException("Invalid LocalTime specified in microseconds");
|
|
}
|
|
|
|
hours = parsed_hours;
|
|
minutes = GetAndSubtractDuration<std::chrono::minutes>(chrono_microseconds);
|
|
seconds = GetAndSubtractDuration<std::chrono::seconds>(chrono_microseconds);
|
|
milliseconds = GetAndSubtractDuration<std::chrono::milliseconds>(chrono_microseconds);
|
|
this->microseconds = chrono_microseconds.count();
|
|
}
|
|
|
|
LocalTime::LocalTime(const LocalTimeParameters &local_time_parameters) {
|
|
if (!IsInBounds(0, 23, local_time_parameters.hours)) {
|
|
throw utils::BasicException("Creating a LocalTime with invalid hour parameter.");
|
|
}
|
|
|
|
if (!IsInBounds(0, 59, local_time_parameters.minutes)) {
|
|
throw utils::BasicException("Creating a LocalTime with invalid minutes parameter.");
|
|
}
|
|
|
|
// ISO 8601 supports leap seconds, but we ignore it for now to simplify the implementation
|
|
if (!IsInBounds(0, 59, local_time_parameters.seconds)) {
|
|
throw utils::BasicException("Creating a LocalTime with invalid seconds parameter.");
|
|
}
|
|
|
|
if (!IsInBounds(0, 999, local_time_parameters.milliseconds)) {
|
|
throw utils::BasicException("Creating a LocalTime with invalid milliseconds parameter.");
|
|
}
|
|
|
|
if (!IsInBounds(0, 999, local_time_parameters.microseconds)) {
|
|
throw utils::BasicException("Creating a LocalTime with invalid microseconds parameter.");
|
|
}
|
|
|
|
hours = local_time_parameters.hours;
|
|
minutes = local_time_parameters.minutes;
|
|
seconds = local_time_parameters.seconds;
|
|
milliseconds = local_time_parameters.milliseconds;
|
|
microseconds = local_time_parameters.microseconds;
|
|
}
|
|
|
|
std::chrono::microseconds LocalTime::SumLocalTimeParts() const {
|
|
namespace chrono = std::chrono;
|
|
return chrono::hours{hours} + chrono::minutes{minutes} + chrono::seconds{seconds} +
|
|
chrono::milliseconds{milliseconds} + chrono::microseconds{microseconds};
|
|
}
|
|
|
|
int64_t LocalTime::MicrosecondsSinceEpoch() const { return SumLocalTimeParts().count(); }
|
|
|
|
int64_t LocalTime::NanosecondsSinceEpoch() const {
|
|
namespace chrono = std::chrono;
|
|
return chrono::duration_cast<chrono::nanoseconds>(SumLocalTimeParts()).count();
|
|
}
|
|
|
|
size_t LocalTimeHash::operator()(const LocalTime &local_time) const {
|
|
utils::HashCombine<uint64_t, uint64_t> hasher;
|
|
size_t result = hasher(0, local_time.hours);
|
|
result = hasher(result, local_time.minutes);
|
|
result = hasher(result, local_time.seconds);
|
|
result = hasher(result, local_time.milliseconds);
|
|
result = hasher(result, local_time.microseconds);
|
|
return result;
|
|
}
|
|
|
|
namespace {
|
|
constexpr auto *kSupportedLocalDateTimeFormatsHelpMessage = R"help(
|
|
String representing the LocalDateTime should be in one of the following formats:
|
|
|
|
- YYYY-MM-DDThh:mm:ss
|
|
- YYYY-MM-DDThh:mm
|
|
|
|
or
|
|
|
|
- YYYYMMDDThhmmss
|
|
- YYYYMMDDThhmm
|
|
- YYYYMMDDThh
|
|
|
|
Symbol table:
|
|
|---|---------|
|
|
| Y | YEAR |
|
|
|---|---------|
|
|
| M | MONTH |
|
|
|---|---------|
|
|
| D | DAY |
|
|
|---|---------|
|
|
| h | HOURS |
|
|
|---|---------|
|
|
| m | MINUTES |
|
|
|---|---------|
|
|
| s | SECONDS |
|
|
|---|---------|
|
|
|
|
Additionally, seconds can be defined as decimal fractions with 3 or 6 digits after the decimal point.
|
|
First 3 digits represent milliseconds, while the second 3 digits represent microseconds.
|
|
|
|
It's important to note that the date and time parts should use both the corresponding separators
|
|
or both parts should be written in their basic forms without the separators.)help";
|
|
|
|
} // namespace
|
|
std::pair<DateParameters, LocalTimeParameters> ParseLocalDateTimeParameters(std::string_view string) {
|
|
auto t_position = string.find('T');
|
|
if (t_position == std::string_view::npos) {
|
|
throw utils::BasicException("Invalid LocalDateTime format. {}", kSupportedLocalDateTimeFormatsHelpMessage);
|
|
}
|
|
|
|
try {
|
|
auto [date_parameters, extended_date_format] = ParseDateParameters(string.substr(0, t_position));
|
|
// https://en.wikipedia.org/wiki/ISO_8601#Combined_date_and_time_representations
|
|
// ISO8601 specifies that you cannot mix extended and basic format of date and time
|
|
// If the date is in the extended format, same must be true for the time, so we don't send T
|
|
// which denotes the basic format. The opposite case also aplies.
|
|
auto local_time_substring = string.substr(t_position + 1);
|
|
if (local_time_substring.empty()) {
|
|
throw utils::BasicException("Invalid LocalDateTime format. {}", kSupportedLocalDateTimeFormatsHelpMessage);
|
|
}
|
|
|
|
auto [local_time_parameters, extended_time_format] = ParseLocalTimeParameters(local_time_substring);
|
|
|
|
if (extended_date_format ^ extended_time_format) {
|
|
throw utils::BasicException(
|
|
"Invalid LocalDateTime format. Both date and time should be in the basic or extended format. {}",
|
|
kSupportedLocalDateTimeFormatsHelpMessage);
|
|
}
|
|
|
|
return {date_parameters, local_time_parameters};
|
|
} catch (const utils::BasicException &e) {
|
|
throw utils::BasicException("Invalid LocalDateTime format. {}", kSupportedLocalDateTimeFormatsHelpMessage);
|
|
}
|
|
}
|
|
|
|
LocalDateTime::LocalDateTime(const int64_t microseconds) {
|
|
auto chrono_microseconds = std::chrono::microseconds(microseconds);
|
|
date = Date(chrono_microseconds.count());
|
|
chrono_microseconds -= std::chrono::microseconds{date.MicrosecondsSinceEpoch()};
|
|
local_time = LocalTime(chrono_microseconds.count());
|
|
}
|
|
|
|
// return microseconds normilized with regard to epoch time point
|
|
int64_t LocalDateTime::MicrosecondsSinceEpoch() const {
|
|
return date.MicrosecondsSinceEpoch() + local_time.MicrosecondsSinceEpoch();
|
|
}
|
|
|
|
int64_t LocalDateTime::SecondsSinceEpoch() const {
|
|
namespace chrono = std::chrono;
|
|
const auto to_sec = chrono::duration_cast<chrono::seconds>(DaysSinceEpoch(date.years, date.months, date.days));
|
|
const auto local_time_seconds =
|
|
chrono::hours(local_time.hours) + chrono::minutes(local_time.minutes) + chrono::seconds(local_time.seconds);
|
|
return (to_sec + local_time_seconds).count();
|
|
}
|
|
|
|
int64_t LocalDateTime::SubSecondsAsNanoseconds() const {
|
|
namespace chrono = std::chrono;
|
|
const auto milli_as_nanos = chrono::duration_cast<chrono::nanoseconds>(chrono::milliseconds(local_time.milliseconds));
|
|
const auto micros_as_nanos =
|
|
chrono::duration_cast<chrono::nanoseconds>(chrono::microseconds(local_time.microseconds));
|
|
|
|
return (milli_as_nanos + micros_as_nanos).count();
|
|
}
|
|
|
|
LocalDateTime::LocalDateTime(const DateParameters date_parameters, const LocalTimeParameters &local_time_parameters)
|
|
: date(date_parameters), local_time(local_time_parameters) {}
|
|
|
|
LocalDateTime::LocalDateTime(const Date &date, const LocalTime &local_time) : date(date), local_time(local_time) {}
|
|
|
|
size_t LocalDateTimeHash::operator()(const LocalDateTime &local_date_time) const {
|
|
utils::HashCombine<uint64_t, uint64_t> hasher;
|
|
size_t result = hasher(0, LocalTimeHash{}(local_date_time.local_time));
|
|
result = hasher(result, DateHash{}(local_date_time.date));
|
|
return result;
|
|
}
|
|
|
|
namespace {
|
|
std::optional<DurationParameters> TryParseIsoDurationString(std::string_view string) {
|
|
DurationParameters duration_parameters;
|
|
|
|
if (string.empty()) {
|
|
return std::nullopt;
|
|
}
|
|
|
|
if (string.front() != 'P') {
|
|
return std::nullopt;
|
|
}
|
|
string.remove_prefix(1);
|
|
|
|
if (string.empty()) {
|
|
return std::nullopt;
|
|
}
|
|
|
|
bool decimal_point_used = false;
|
|
|
|
const auto check_decimal_fraction = [&](const auto substring) {
|
|
// only the last number in the string can be a fraction
|
|
// if a decimal point was already found, and another number is being parsed
|
|
// we are in an invalid state
|
|
if (decimal_point_used) {
|
|
return false;
|
|
}
|
|
|
|
decimal_point_used = substring.find('.') != std::string_view::npos;
|
|
return true;
|
|
};
|
|
|
|
const auto parse_and_assign = [&](std::string_view &string, const char label, double &destination) {
|
|
auto label_position = string.find(label);
|
|
if (label_position == std::string_view::npos) {
|
|
return true;
|
|
}
|
|
|
|
const auto number_substring = string.substr(0, label_position);
|
|
if (!check_decimal_fraction(number_substring)) {
|
|
return false;
|
|
}
|
|
|
|
const auto maybe_parsed_number = ParseNumber<double>(number_substring, number_substring.size());
|
|
if (!maybe_parsed_number) {
|
|
return false;
|
|
}
|
|
destination = *maybe_parsed_number;
|
|
// remove number + label
|
|
string.remove_prefix(label_position + 1);
|
|
return true;
|
|
};
|
|
|
|
const auto parse_duration_date_part = [&](auto date_string) {
|
|
if (!std::isdigit(date_string.front())) {
|
|
return false;
|
|
throw utils::BasicException("Invalid format of duration string");
|
|
}
|
|
|
|
if (!parse_and_assign(date_string, 'Y', duration_parameters.years)) {
|
|
return false;
|
|
}
|
|
if (date_string.empty()) {
|
|
return true;
|
|
}
|
|
|
|
if (!parse_and_assign(date_string, 'M', duration_parameters.months)) {
|
|
return false;
|
|
}
|
|
if (date_string.empty()) {
|
|
return true;
|
|
}
|
|
|
|
if (!parse_and_assign(date_string, 'D', duration_parameters.days)) {
|
|
return false;
|
|
}
|
|
|
|
return date_string.empty();
|
|
};
|
|
|
|
const auto parse_duration_time_part = [&](auto time_string) {
|
|
if (!std::isdigit(time_string.front())) {
|
|
return false;
|
|
}
|
|
|
|
if (!parse_and_assign(time_string, 'H', duration_parameters.hours)) {
|
|
return false;
|
|
}
|
|
if (time_string.empty()) {
|
|
return true;
|
|
}
|
|
|
|
if (!parse_and_assign(time_string, 'M', duration_parameters.minutes)) {
|
|
return false;
|
|
}
|
|
if (time_string.empty()) {
|
|
return true;
|
|
}
|
|
|
|
if (!parse_and_assign(time_string, 'S', duration_parameters.seconds)) {
|
|
return false;
|
|
}
|
|
|
|
return time_string.empty();
|
|
};
|
|
|
|
auto t_position = string.find('T');
|
|
|
|
const auto date_string = string.substr(0, t_position);
|
|
if (!date_string.empty() && !parse_duration_date_part(date_string)) {
|
|
return std::nullopt;
|
|
}
|
|
|
|
if (t_position == std::string_view::npos) {
|
|
return duration_parameters;
|
|
}
|
|
|
|
const auto time_string = string.substr(t_position + 1);
|
|
if (time_string.empty() || !parse_duration_time_part(time_string)) {
|
|
return std::nullopt;
|
|
}
|
|
|
|
return duration_parameters;
|
|
}
|
|
} // namespace
|
|
|
|
// clang-format off
|
|
const auto kSupportedDurationFormatsHelpMessage = fmt::format(R"help(
|
|
"String representing duration should be in the following format:
|
|
|
|
P[nY][nM][nD]T[nH][nM][nS]
|
|
|
|
Symbol table:
|
|
|---|---------|
|
|
| Y | YEAR |
|
|
|---|---------|
|
|
| M | MONTH |
|
|
|---|---------|
|
|
| D | DAY |
|
|
|---|---------|
|
|
| H | HOURS |
|
|
|---|---------|
|
|
| M | MINUTES |
|
|
|---|---------|
|
|
| S | SECONDS |
|
|
|---|---------|
|
|
|
|
'n' represents a number that can be an integer of ANY value, or a fraction IF it's the last value in the string.
|
|
All the fields are optional.
|
|
|
|
Alternatively, the string can contain LocalDateTime format:
|
|
P<local_date_time_string>
|
|
{}
|
|
)help", kSupportedLocalDateTimeFormatsHelpMessage);
|
|
// clang-format on
|
|
|
|
DurationParameters ParseDurationParameters(std::string_view string) {
|
|
// https://en.wikipedia.org/wiki/ISO_8601#Durations
|
|
// The string needs to start with P followed by one of the two options:
|
|
// - string in a duration specific format
|
|
// - string in a combined date and time format (LocalDateTime string format)
|
|
if (string.empty() || string.front() != 'P') {
|
|
throw utils::BasicException("Duration string is empty.");
|
|
}
|
|
|
|
if (auto maybe_duration_parameters = TryParseIsoDurationString(string); maybe_duration_parameters) {
|
|
return *maybe_duration_parameters;
|
|
}
|
|
|
|
DurationParameters duration_parameters;
|
|
// remove P and try to parse local date time
|
|
string.remove_prefix(1);
|
|
|
|
try {
|
|
const auto [date_parameters, local_time_parameters] = ParseLocalDateTimeParameters(string);
|
|
|
|
duration_parameters.years = static_cast<double>(date_parameters.years);
|
|
duration_parameters.months = static_cast<double>(date_parameters.months);
|
|
duration_parameters.days = static_cast<double>(date_parameters.days);
|
|
duration_parameters.hours = static_cast<double>(local_time_parameters.hours);
|
|
duration_parameters.minutes = static_cast<double>(local_time_parameters.minutes);
|
|
duration_parameters.seconds = static_cast<double>(local_time_parameters.seconds);
|
|
duration_parameters.milliseconds = static_cast<double>(local_time_parameters.milliseconds);
|
|
duration_parameters.microseconds = static_cast<double>(local_time_parameters.microseconds);
|
|
|
|
return duration_parameters;
|
|
} catch (const utils::BasicException &e) {
|
|
throw utils::BasicException("Invalid duration string. {}", kSupportedDurationFormatsHelpMessage);
|
|
}
|
|
}
|
|
|
|
namespace {
|
|
template <Chrono From, Chrono To>
|
|
constexpr To CastChronoDouble(const double value) {
|
|
return std::chrono::duration_cast<To>(std::chrono::duration<double, typename From::period>(value));
|
|
};
|
|
} // namespace
|
|
|
|
Duration::Duration(int64_t microseconds) { this->microseconds = microseconds; }
|
|
|
|
Duration::Duration(const DurationParameters ¶meters) {
|
|
microseconds = (CastChronoDouble<std::chrono::years, std::chrono::microseconds>(parameters.years) +
|
|
CastChronoDouble<std::chrono::months, std::chrono::microseconds>(parameters.months) +
|
|
CastChronoDouble<std::chrono::days, std::chrono::microseconds>(parameters.days) +
|
|
CastChronoDouble<std::chrono::hours, std::chrono::microseconds>(parameters.hours) +
|
|
CastChronoDouble<std::chrono::minutes, std::chrono::microseconds>(parameters.minutes) +
|
|
CastChronoDouble<std::chrono::seconds, std::chrono::microseconds>(parameters.seconds) +
|
|
CastChronoDouble<std::chrono::milliseconds, std::chrono::microseconds>(parameters.milliseconds) +
|
|
CastChronoDouble<std::chrono::microseconds, std::chrono::microseconds>(parameters.microseconds))
|
|
.count();
|
|
}
|
|
|
|
int64_t Duration::Months() const {
|
|
std::chrono::microseconds ms(microseconds);
|
|
return std::chrono::duration_cast<std::chrono::months>(ms).count();
|
|
}
|
|
|
|
int64_t Duration::SubMonthsAsDays() const {
|
|
namespace chrono = std::chrono;
|
|
const auto months = chrono::months(Months());
|
|
const auto micros = chrono::microseconds(microseconds);
|
|
return chrono::duration_cast<chrono::days>(micros - months).count();
|
|
}
|
|
|
|
int64_t Duration::SubDaysAsSeconds() const {
|
|
namespace chrono = std::chrono;
|
|
const auto months = chrono::months(Months());
|
|
const auto days = chrono::days(SubMonthsAsDays());
|
|
const auto micros = chrono::microseconds(microseconds);
|
|
return chrono::duration_cast<chrono::seconds>(micros - months - days).count();
|
|
}
|
|
|
|
int64_t Duration::SubSecondsAsNanoseconds() const {
|
|
namespace chrono = std::chrono;
|
|
const auto months = chrono::months(Months());
|
|
const auto days = chrono::days(SubMonthsAsDays());
|
|
const auto secs = chrono::seconds(SubDaysAsSeconds());
|
|
const auto micros = chrono::microseconds(microseconds);
|
|
return chrono::duration_cast<chrono::nanoseconds>(micros - months - days - secs).count();
|
|
}
|
|
|
|
Duration Duration::operator-() const {
|
|
Duration result{-microseconds};
|
|
return result;
|
|
}
|
|
|
|
size_t DurationHash::operator()(const Duration &duration) const { return std::hash<int64_t>{}(duration.microseconds); }
|
|
|
|
} // namespace utils
|