/* * This file is a part of PikoTools * and is distributed under the 2-Clause BSD licence. * Author: Tomasz Sowa */ /* * Copyright (c) 2012-2023, Tomasz Sowa * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. * */ #ifndef headerfile_pikotools_src_date_date #define headerfile_pikotools_src_date_date #include #include #include "convert/inttostr.h" namespace pt { /* this class represents a Date (year, month, day, hour, min, sec) it has O(1) algorithm when converting from/to time_t (seconds from the Unix Epoch) algorithm description: http://alcor.concordia.ca/~gpkatch/gdate-algorithm.html http://alcor.concordia.ca/~gpkatch/gdate-method.html current limitation: we do not support leap seconds */ class Date { public: static const time_t ONE_MIN = 60; /* one minute in seconds */ static const time_t ONE_HOUR = ONE_MIN * 60; /* one hour in seconds */ static const time_t ONE_DAY = ONE_HOUR * 24; /* one day in seconds */ static const time_t ONE_WEEK = ONE_DAY * 7; /* one week in seconds */ /* the date */ int year; // 1970 - 10000 int month; // 1 - 12 int day; // 1 - 31 int hour; // 0 - 23 int min; // 0 - 59 int sec; // 0 - 59 /* default c-ctor sets the Unix Epoch (Clear method): 1970.01.01 00:00:00 */ Date(); /* converting from Date, time_t (seconds from the Unix Epoch), tm structure, and strings */ Date(const Date & d); Date(time_t t); Date(const tm & t); Date(const char * str); Date(const wchar_t * str); Date(const std::string & str); Date(const std::wstring & str); Date & operator=(const Date & d); Date & operator=(time_t t); Date & operator=(const tm & t); Date & operator=(const char * str); Date & operator=(const wchar_t * str); Date & operator=(const std::string & str); Date & operator=(const std::wstring & str); /* adding/subtracting time_t (seconds from the Unix Epoch) */ Date operator+(time_t t) const; Date operator-(time_t t) const; Date & operator+=(time_t t); Date & operator-=(time_t t); /* swapping the contents of *this with date */ void Swap(Date & date); /* converts time_t in seconds (from the Unix Epoch) to this object */ void FromTime(time_t t); /* converts tm structure to this object */ void FromTm(const tm & t); /* returns time_t (in seconds from the Unix Epoch) */ time_t ToTime() const; /* return tm structure only tm_year, tm_mon, tm_mday, tm_hour, tm_min, tm_sec fields are set the rest is equal to zero */ tm ToTm() const; /* getting/setting the number of days from 0000:03:01 (year 0, month 3 - March, day 1) (ToDays() and FromDays() can work even with a year less than 1970) */ long long ToDays() const; void FromDays(long long g); /* returns a difference in second between two dates (always a value greater than zero) */ time_t operator-(const Date & d) const; /* 'Compare' methods returns zero if this and d are equal return value less than zero if this is lower than d and a value greater than zero if this is greater than d */ int CompareDate(const Date & d, bool ignore_year = false) const; int CompareTime(const Date & d) const; int Compare(const Date & d, bool ignore_year = false) const; /* returns true if year, month and day are the same */ bool IsTheSameDay(const Date & d) const; /* returns true if hour, min and sec are the same */ bool IsTheSameHour(const Date & d) const; /* operators for comparing */ bool operator==(const Date & d) const; bool operator!=(const Date & d) const; bool operator>(const Date & d) const; bool operator>=(const Date & d) const; bool operator<(const Date & d) const; bool operator<=(const Date & d) const; /* set the date to: 1970-01-01 the time is not changed */ void ClearDate(); /* set the time to 00:00:00 the date is not changed */ void ClearTime(); /* set the Unix Epoch: 1970-01-01 00:00:00 */ void Clear(); /* assert a correct date (values will be from the correct range) year: 1970 - 10000 month: 1 - 12 day: 1 - MonthLen hour: 0 - 23 min: 0 - 59 sec: 0 - 59 */ void AssertCorrectDate(); /* return true if values are from the correct range year: 1970 - 10000 month: 1 - 12 day: 1 - MonthLen hour: 0 - 23 min: 0 - 59 sec: 0 - 59 */ bool IsCorrectDate(); /* returns how many days there is in a month y - year 1970 - ... m - month 1-12 */ static int MonthLen(int y, int m); /* returns true if 'y' is a leap year leap year has one additional day (in february) - so the year lasts 366 days */ static bool IsYearLeap(int y); /* returns true if the currecn year is a leap year */ bool IsYearLeap() const; /* returns a day index from a week sunday - 0 monday - 1 ... saturday - 6 */ int WeekDay() const; /* this method outputs to the given stream the number of a month in Roman numerals e.g: if month is equal to 3 then III will be put to 'out' month should be in the range [1,12] */ template static void SerializeMonthAsRoman(Stream & out, int month); /* this method outputs to the given stream: YYYY-MM-DD, eg. 1990-02-12 ISO 8601 format */ template void SerializeYearMonthDay(Stream & out, bool roman_month = false) const; /* this method outputs to the given stream: HH:MM:SS, eg: 13:05:39 ISO 8601 format */ template void SerializeHourMinSec(Stream & out) const; /* this method outputs to the given stream: MM-DD, eg. 02-12 (02 month, 12 day) */ template void SerializeMonthDay(Stream & out, bool roman_month = false) const; /* this method outputs to the given stream: HH:MM, eg: 13:05 */ template void SerializeHourMin(Stream & out) const; /* this method outputs to the given stream: YYYY-MM-DD HH:MM:SS, eg: 1990-02-12 13:05:39 ISO 8601 format */ template void Serialize(Stream & out, bool roman_month = false, bool with_seconds = true) const; /* this method outputs to the given stream: YYYY-MM-DDTHH:MM:SSZ, eg: 1990-02-12T13:05:39Z ISO 8601 format */ template void SerializeISO(Stream & out) const; /* parsing day month and year the input string can be as follows: "12-10-2008" white characters are ommited and the method stops after reading the year so the input string can be: " 12 - 10 - 2008some text " a white character means a space or a tab as a separator can be '-' '/' or '.' so below strings have the same meaning: " 12.10.2008 " " 12/10 / 2008 " this method doesn't test if the values are correct use IsCorrectDate() to check */ template bool ParseDayMonthYear(const CStringType * str, const CStringType ** str_after = nullptr); template bool ParseDayMonthYear(const StringType & str); /* parsing year month and day the input string can be as follows: "2008-10-12" white characters are ommited and the method stops after reading the day so the input string can be: " 2008 - 10 - 12some text " a white character means a space or a tab as a separator can be '-' '/' or '.' so below strings have the same meaning: " 2008.10.12 " " 2008/10 / 12 " this method doesn't test if the values are correct use IsCorrectDate() to check */ template bool ParseYearMonthDay(const CStringType * str, const CStringType ** str_after = nullptr); template bool ParseYearMonthDay(const StringType & str); /* parsing month and day the input string can be as follows: "10-12" (month: 10, day: 12) white characters are ommited and the method stops after reading the day so the input string can be: " 10 - 12some text " a white character means a space or a tab as a separator can be '-' '/' or '.' so below strings have the same meaning: " 10.12 " " 10 / 12 " this method doesn't test if the values are correct use IsCorrectDate() to check */ template bool ParseMonthDay(const CStringType * str, const CStringType ** str_after = nullptr); template bool ParseMonthDay(const StringType & str); /* parsing hour minutes and seconds the input string can be as follows: "14:10:35" white characters are ommited and the method stops after reading seconds so the input string can be: " 14 : 10 : 35some text " a white character means a space or a tab a separator is only the ':' character this method doesn't test if the values are correct use IsCorrectDate() to check */ template bool ParseHourMinSec(const CStringType * str, const CStringType ** str_after = nullptr); template bool ParseHourMinSec(const StringType & str); /* parsing hour and minutes the input string can be as follows: "14:10" white characters are ommited and the method stops after reading minutes so the input string can be: " 14 : 10some text " a white character means a space or a tab a separator is only the ':' character this method doesn't test if the values are correct use IsCorrectDate() to check */ template bool ParseHourMin(const CStringType * str, const CStringType ** str_after = nullptr); template bool ParseHourMin(const StringType & str); template bool ParseZoneOffset(const CStringType * str, const CStringType ** str_after = nullptr); template bool ParseZoneOffset(const StringType & str); /* parsing a year and a month (if exists) and a day (if exists) the input string can be as follows: YYYY[sep]MM[sep]DD the separator is optional, it can be: '.', '-', '/' or just white characters (white characters before and after a separator are skipped) sample valid dates: "2022" " 2022" "202212" " 2022 12" "2022-12" " 2022 - 12" "20221222" " 20221222 " " 2022 12 22 " " 2022 12 22 " "2022-12-22" " 2022-12-22" " 2022 - 12 - 22 " if the month is not provided then it is set to 01, if the day is not provided then it is set to 01 too this method doesn't test if the values are correct use IsCorrectDate() to check */ template bool ParseDate(const CStringType * str, const CStringType ** str_after = nullptr); template bool ParseDate(const StringType & str); /* parsing an hour and minutes (if exists) and seconds (if exists) the input string can be as follows: HH[sep]MM[sep]SS separator is optional, it can be ':' or just white characters (white characters before and after a separator are skipped) "14" -- only an hour given (min and sec will be zero) "14:10" -- hour with minutes (sec will be zero) "1410" -- the same as above "14:10:35" -- hour, minutes and seconds "141035" -- the same as above "14 10 35" -- the same as above white characters are ommited so these are valid strings too: " 14 : 10 : 35 " " 14 : 10 : 35some text " a white character means a space or a tab a decimal fraction may be added to the lowest order time element present, e.g: "14.500" = 14:30:00 "14:10.500" = 14:10:30 "14:10:35.500" = 14:10:35 (the seconds' fraction is skipped) instead of dot you can use a comma too, the fraction part can be from 1 to 9 digits this method doesn't test if the values are correct use IsCorrectDate() to check */ template bool ParseTime(const CStringType * str, const CStringType ** str_after = nullptr); template bool ParseTime(const StringType & str); /* parsing month, day, hour and minutes (if exists) and seconds (if exists) the input string can be as follows: "10-23 14" -- only month, day and hour given (min and sec will be zero) "10-23 14:10" -- month, day and hour with minutes (sec will be zero) "10-23 14:10:35" -- month, day, hour, minutes and seconds white characters are ommited so these are valid strings too: " 10 - 23 14 : 10 : 35 " " 10 - 23 14 : 10 : 35some text " a white character means a space or a tab this method doesn't test if the values are correct use IsCorrectDate() to check the time is parsed with ParseTime() method, look there for an additional description about available time strings */ template bool ParseMonthDayTime(const CStringType * str, const CStringType ** str_after = nullptr); template bool ParseMonthDayTime(const StringType & str); /* parsing year/month/day hour:min:sec the input strings can be as follows: "20081012 141035" "20081012T141035" "20081012141035" "2008-10-12 14:10:35" "2008/10/12 14:10:35" "2008.10.12 14:10:35" "2008-10/12 14:10:35" "2008-10-12 14:10:35.500" "2008-10-12 14:10.500" "2008-10-12 14.500" "2008-10-12 14" "2008-10 14" "2008 14" months and days can be omitted - in such a case 01 is set, similar min and sec can be omitted (they are assumed to be 00 in such a case) white characters are ommited so the input string can be: " 2008 - 10 / 12 14 : 10 : 35 " a white character means a space or a tab as an optional separator for the date can be '-' '/' or '.' as an optional separator for the time is the ':' character at the and of the string there can be defined a time zone in the form of "+01:00", e.g: "2008-10-12 14:10:35+01:00" "2008-10-12 14:10:35-02:00" or without a separator: "2008-10-12 14:10:35+0100" "2008-10-12 14:10:35-0200" or just a 'Z' character indicating 00:00 zone "2008-10-12 14:10:35Z" at the end the method checks if the values are correct (by using IsCorrectDate()) the format is similar to ISO 8601 https://en.wikipedia.org/wiki/ISO_8601 at the moment there is no support for week dates e.g. "2008-W01-3" and ordinal dates e.g. "2008-010" formats see ParseDate() and ParseTime for more examples */ template bool Parse(const CStringType * str, const CStringType ** str_after, bool check_time_zone = true); template bool Parse(const CStringType * str, bool check_time_zone = true); template bool Parse(const StringType & str, bool check_time_zone = true); private: void AssertRange(int & val, int val_min, int val_max); template void SerializeInt(Stream & out, int val, size_t min_width) const; template void SerializeInt(Stream & out, int val) const; template void SetAfter(const CStringType * str, const CStringType ** str_after); template void SkipWhite(const CStringType * & str); template bool IsDigit(const CStringType * str); template bool ReadInt(const CStringType * & str, int & result, size_t max_digits = 0, size_t * digits_read = nullptr, int * digits_base = nullptr); template bool ReadCommaInt(const CStringType * & str, bool & was_comma, int & result, size_t max_digits = 0, size_t * digits_read = nullptr, int * base = nullptr); template bool ParseTimeValue(const CStringType * & str, int & value, bool & has_fraction, int & fraction, int * fraction_base); template bool SkipSeparator(const CStringType * & str, int separator, int separator2 = -1, int separator3 = -1); }; template void Date::SerializeInt(Stream & out, int val, size_t min_width) const { char buf[64]; size_t len; if( Toa(val, buf, sizeof(buf) / sizeof(char), 10, &len) ) { for(size_t i = len ; i < min_width ; ++i) { out << '0'; } out << buf; } } template void Date::SerializeInt(Stream & out, int val) const { SerializeInt(out, val, 2); } template void Date::SerializeMonthAsRoman(Stream & out, int month) { const char * month_roman[] = { "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X", "XI", "XII" }; if( month >= 1 && month <= 12 ) { const char * month_str = month_roman[month - 1]; for( ; *month_str ; ++month_str) out << *month_str; } } template void Date::SerializeYearMonthDay(Stream & out, bool roman_month) const { SerializeInt(out, year, 4); out << '-'; if( roman_month ) SerializeMonthAsRoman(out, month); else SerializeInt(out, month); out << '-'; SerializeInt(out, day); } template void Date::SerializeHourMinSec(Stream & out) const { SerializeInt(out, hour); out << ':'; SerializeInt(out, min); out << ':'; SerializeInt(out, sec); } template void Date::SerializeMonthDay(Stream & out, bool roman_month) const { if( roman_month ) SerializeMonthAsRoman(out, month); else SerializeInt(out, month); out << '-'; SerializeInt(out, day); } template void Date::SerializeHourMin(Stream & out) const { SerializeInt(out, hour); out << ':'; SerializeInt(out, min); } template void Date::Serialize(Stream & out, bool roman_month, bool with_seconds) const { SerializeYearMonthDay(out, roman_month); out << ' '; if( with_seconds ) SerializeHourMinSec(out); else SerializeHourMin(out); } template void Date::SerializeISO(Stream & out) const { SerializeYearMonthDay(out, false); out << 'T'; SerializeHourMinSec(out); out << 'Z'; } template bool Date::ParseDayMonthYear(const CStringType * str, const CStringType ** str_after) { bool result = false; if( ReadInt(str, day) && SkipSeparator(str, '.', '-', '/') ) if( ReadInt(str, month) && SkipSeparator(str, '.', '-', '/') ) if( ReadInt(str, year) ) result = true; SetAfter(str, str_after); return result; } template bool Date::ParseDayMonthYear(const StringType & str) { return ParseDayMonthYear(str.c_str()); } template bool Date::ParseYearMonthDay(const CStringType * str, const CStringType ** str_after) { bool result = false; if( ReadInt(str, year) && SkipSeparator(str, '.', '-', '/') ) if( ReadInt(str, month) && SkipSeparator(str, '.', '-', '/') ) if( ReadInt(str, day) ) result = true; SetAfter(str, str_after); return result; } template bool Date::ParseYearMonthDay(const StringType & str) { return ParseYearMonthDay(str.c_str()); } template bool Date::ParseMonthDay(const CStringType * str, const CStringType ** str_after) { bool result = false; if( ReadInt(str, month) && SkipSeparator(str, '.', '-', '/') ) if( ReadInt(str, day) ) result = true; SetAfter(str, str_after); return result; } template bool Date::ParseMonthDay(const StringType & str) { return ParseMonthDay(str.c_str()); } template bool Date::ParseHourMinSec(const CStringType * str, const CStringType ** str_after) { bool result = false; if( ReadInt(str, hour) && SkipSeparator(str, ':') ) if( ReadInt(str, min) && SkipSeparator(str, ':') ) if( ReadInt(str, sec) ) result = true; SetAfter(str, str_after); return result; } template bool Date::ParseHourMinSec(const StringType & str) { return ParseHourMinSec(str.c_str()); } template bool Date::ParseHourMin(const CStringType * str, const CStringType ** str_after) { bool result = false; if( ReadInt(str, hour) && SkipSeparator(str, ':') ) if( ReadInt(str, min) ) result = true; SetAfter(str, str_after); return result; } template bool Date::ParseHourMin(const StringType & str) { return ParseHourMin(str.c_str()); } template bool Date::ParseZoneOffset(const CStringType * str, const CStringType ** str_after) { bool result = false; bool is_sign = false; int offset_hour = 0; int offset_min = 0; SkipWhite(str); if( *str == '-' || *str == '+' ) { if( *str == '-' ) is_sign = true; str += 1; result = true; // if there are no digits we return true if( IsDigit(str) ) { result = ReadInt(str, offset_hour, 2) && offset_hour >= -12 && offset_hour <= 14; if( result ) { SkipSeparator(str, ':'); if( IsDigit(str) ) { // offset_min is optional result = ReadInt(str, offset_min, 2) && offset_min > -60 && offset_min < 60; } if( result ) { time_t offset = (time_t)offset_hour * 60 * 60 + (time_t)offset_min * 60; if( is_sign ) offset = -offset; FromTime(ToTime() - offset); } } } } SetAfter(str, str_after); return result; } template bool Date::ParseZoneOffset(const StringType & str) { return ParseZoneOffset(str.c_str()); } template bool Date::ParseDate(const CStringType * str, const CStringType ** str_after) { bool status = false; ClearDate(); if( ReadInt(str, year, 4) ) { status = true; SkipSeparator(str, '.', '-', '/'); if( IsDigit(str) ) { status = ReadInt(str, month, 2); SkipSeparator(str, '.', '-', '/'); if( status && IsDigit(str) ) { status = ReadInt(str, day, 2); } } } SetAfter(str, str_after); return status; } template bool Date::ParseDate(const StringType & str) { return ParseDate(str.c_str()); } template bool Date::ParseTimeValue(const CStringType * & str, int & value, bool & has_fraction, int & fraction, int * fraction_base) { bool status = false; size_t digits_read = 0; bool was_comma = false; has_fraction = false; fraction = 0; *fraction_base = 1; if( ReadInt(str, value, 2) ) { status = ReadCommaInt(str, was_comma, fraction, 0, &digits_read, fraction_base); if( status ) { if( was_comma ) has_fraction = true; SkipSeparator(str, ':'); } } return status; } template bool Date::ParseTime(const CStringType * str, const CStringType ** str_after) { bool status = false; bool has_fraction = false; int fraction = 0; int fraction_base = 0; ClearTime(); if( ParseTimeValue(str, hour, has_fraction, fraction, &fraction_base) ) { status = true; if( has_fraction ) { min = (60L * fraction) / (long)(fraction_base); int min_rem = (60L * fraction) % (long)(fraction_base); sec = (min_rem * 60L) / (long)fraction_base; } else if( IsDigit(str) ) { status = ParseTimeValue(str, min, has_fraction, fraction, &fraction_base); if( status ) { if( has_fraction ) { sec = (60L * fraction) / (long)(fraction_base); } else if( IsDigit(str) ) { status = ParseTimeValue(str, sec, has_fraction, fraction, &fraction_base); // ignore the seconds fraction if exists } } } } SetAfter(str, str_after); return status; } template bool Date::ParseTime(const StringType & str) { return ParseTime(str.c_str()); } template bool Date::ParseMonthDayTime(const CStringType * str, const CStringType ** str_after) { const CStringType * after; bool result = false; if( ParseMonthDay(str, &after) ) if( ParseTime(after, &after) ) result = true; SetAfter(after, str_after); return result; } template bool Date::ParseMonthDayTime(const StringType & str) { return ParseMonthDayTime(str.c_str()); } template bool Date::Parse(const CStringType * str, const CStringType ** str_after, bool check_time_zone) { const CStringType * after; bool result = false; Clear(); if( ParseDate(str, &after) ) { result = true; SkipWhite(after); if( *after == 'T' ) { after += 1; SkipWhite(after); } if( IsDigit(after) ) { result = ParseTime(after, &after); if( result && check_time_zone ) { SkipWhite(after); if( *after == 'Z' ) { after += 1; } else if( *after == '-' || *after == '+' ) { result = ParseZoneOffset(after, &after); } } } } SetAfter(after, str_after); if( result ) result = IsCorrectDate(); return result; } template bool Date::Parse(const CStringType * str, bool check_time_zone) { const CStringType * str_after = nullptr; return Parse(str, &str_after, check_time_zone); } template bool Date::Parse(const StringType & str, bool check_time_zone) { const typename StringType::value_type * after_string = nullptr; return Parse(str.c_str(), &after_string, check_time_zone); } template void Date::SetAfter(const CStringType * str, const CStringType ** str_after) { if( str_after ) *str_after = str; } template void Date::SkipWhite(const CStringType * & str) { while( *str==' ' || *str=='\t' ) str += 1; } template bool Date::IsDigit(const CStringType * str) { return (*str >= '0' && *str <= '9'); } template bool Date::ReadInt(const CStringType * & str, int & result, size_t max_digits, size_t * digits_read, int * digits_base) { SkipWhite(str); result = 0; size_t read_chars = 0; int base = 1; bool skip_last_digits = false; if( max_digits == 0 ) skip_last_digits = true; if( max_digits == 0 || max_digits > 9 ) max_digits = 9; while( IsDigit(str) && read_chars < max_digits ) { result = result * 10 + (*str - '0'); base = base * 10; str += 1; read_chars += 1; } if( skip_last_digits ) { while( IsDigit(str) ) { str += 1; read_chars += 1; } } if( digits_read ) *digits_read = read_chars; if( digits_base ) *digits_base = base; return read_chars > 0 && read_chars <= max_digits; } template bool Date::ReadCommaInt(const CStringType * & str, bool & was_comma, int & result, size_t max_digits, size_t * digits_read, int * base) { bool status = true; // the comma is optional so we return true if it not exists result = 0; was_comma = false; if( digits_read ) *digits_read = 0; if( base ) *base = 1; if( *str == '.' || *str == ',' ) { str += 1; was_comma = true; if( IsDigit(str) ) status = ReadInt(str, result, max_digits, digits_read, base); } return status; } template bool Date::SkipSeparator(const CStringType * & str, int separator, int separator2, int separator3) { SkipWhite(str); if( *str == separator ) { str += 1; SkipWhite(str); return true; } if( separator2 != -1 && *str == separator2 ) { str += 1; SkipWhite(str); return true; } if( separator3 != -1 && *str == separator3 ) { str += 1; SkipWhite(str); return true; } return false; } } // namespace #endif