taskwarrior/src/ISO8601.cpp
2015-08-12 10:07:43 -04:00

912 lines
24 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

////////////////////////////////////////////////////////////////////////////////
//
// Copyright 2006 - 2015, Paul Beckingham, Federico Hernandez.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
// http://www.opensource.org/licenses/mit-license.php
//
////////////////////////////////////////////////////////////////////////////////
#include <cmake.h>
#include <sstream>
#include <stdlib.h>
#include <Lexer.h>
#include <ISO8601.h>
#include <Date.h>
#define DAY 86400
#define HOUR 3600
#define MINUTE 60
#define SECOND 1
static struct
{
std::string unit;
int seconds;
bool standalone;
} durations[] =
{
// These are sorted by first character, then length, so that Nibbler::getOneOf
// returns a maximal match.
{"annual", 365 * DAY, true},
{"biannual", 730 * DAY, true},
{"bimonthly", 61 * DAY, true},
{"biweekly", 14 * DAY, true},
{"biyearly", 730 * DAY, true},
{"daily", 1 * DAY, true},
{"days", 1 * DAY, false},
{"day", 1 * DAY, true},
{"d", 1 * DAY, false},
{"fortnight", 14 * DAY, true},
{"hours", 1 * HOUR, false},
{"hour", 1 * HOUR, true},
{"hrs", 1 * HOUR, false},
{"hr", 1 * HOUR, true},
{"h", 1 * HOUR, false},
{"minutes", 1 * MINUTE, false},
{"minute", 1 * MINUTE, true},
{"mins", 1 * MINUTE, false},
{"min", 1 * MINUTE, true},
{"monthly", 30 * DAY, true},
{"months", 30 * DAY, false},
{"month", 30 * DAY, true},
{"mnths", 30 * DAY, false},
{"mths", 30 * DAY, false},
{"mth", 30 * DAY, true},
{"mos", 30 * DAY, false},
{"mo", 30 * DAY, true},
{"m", 30 * DAY, false},
{"quarterly", 91 * DAY, true},
{"quarters", 91 * DAY, false},
{"quarter", 91 * DAY, true},
{"qrtrs", 91 * DAY, false},
{"qrtr", 91 * DAY, true},
{"qtrs", 91 * DAY, false},
{"qtr", 91 * DAY, true},
{"q", 91 * DAY, false},
{"semiannual", 183 * DAY, true},
{"sennight", 14 * DAY, false},
{"seconds", 1 * SECOND, false},
{"second", 1 * SECOND, true},
{"secs", 1 * SECOND, false},
{"sec", 1 * SECOND, true},
{"s", 1 * SECOND, false},
{"weekdays", 1 * DAY, true},
{"weekly", 7 * DAY, true},
{"weeks", 7 * DAY, false},
{"week", 7 * DAY, true},
{"wks", 7 * DAY, false},
{"wk", 7 * DAY, true},
{"w", 7 * DAY, false},
{"yearly", 365 * DAY, true},
{"years", 365 * DAY, false},
{"year", 365 * DAY, true},
{"yrs", 365 * DAY, false},
{"yr", 365 * DAY, true},
{"y", 365 * DAY, false},
};
#define NUM_DURATIONS (sizeof (durations) / sizeof (durations[0]))
////////////////////////////////////////////////////////////////////////////////
ISO8601d::ISO8601d ()
{
clear ();
}
////////////////////////////////////////////////////////////////////////////////
ISO8601d::~ISO8601d ()
{
}
////////////////////////////////////////////////////////////////////////////////
ISO8601d::operator time_t () const
{
return _value;
}
////////////////////////////////////////////////////////////////////////////////
// Supported:
//
// result ::= date 'T' time 'Z' # UTC
// | date 'T' time # Local
// | date-ext 'T' time-ext 'Z' # UTC
// | date-ext 'T' time-ext offset-ext # Specified TZ
// | date-ext 'T' time-ext # Local
// | date-ext # Local
// | time-ext 'Z'
// | time-ext offset-ext Not needed
// | time-ext
// ;
//
// date-ext ::= ±YYYYY-MM-DD Νot needed
// | ±YYYYY-Www-D Νot needed
// | ±YYYYY-Www Νot needed
// | ±YYYYY-DDD Νot needed
// | YYYY-MM-DD
// | YYYY-DDD
// | YYYY-Www-D
// | YYYY-Www
// ;
//
// time-ext ::= hh:mm:ss[,ss]
// | hh:mm[,mm]
// | hh[,hh] Ambiguous (number)
// ;
//
// time-utc-ext ::= hh:mm[:ss] 'Z' ;
//
// offset-ext ::= ±hh[:mm] ;
//
// Not yet supported:
//
// recurrence ::=
// | 'R' [n] '/' designated '/' datetime-ext # duration end
// | 'R' [n] '/' designated '/' datetime # duration end
// | 'R' [n] '/' designated # duration
// | 'R' [n] '/' datetime-ext '/' designated # start duration
// | 'R' [n] '/' datetime-ext '/' datetime-ext # start end
// | 'R' [n] '/' datetime '/' designated # start duration
// | 'R' [n] '/' datetime '/' datetime # start end
// ;
//
bool ISO8601d::parse (const std::string& input, std::string::size_type& start)
{
auto i = start;
Nibbler n (input.substr (i));
if (parse_date_time (n) || // Strictest first.
parse_date_time_ext (n) ||
parse_date_ext (n) ||
parse_time_utc_ext (n) ||
parse_time_off_ext (n) ||
parse_time_ext (n)) // Time last, as it is the most permissive.
{
// Check the values and determine time_t.
if (validate ())
{
// Record cursor position.
start = n.cursor ();
resolve ();
return true;
}
}
return false;
}
////////////////////////////////////////////////////////////////////////////////
void ISO8601d::clear ()
{
_year = 0;
_month = 0;
_week = 0;
_weekday = 0;
_julian = 0;
_day = 0;
_seconds = 0;
_offset = 0;
_utc = false;
_value = 0;
}
////////////////////////////////////////////////////////////////////////////////
bool ISO8601d::parse_date_time (Nibbler& n)
{
n.save ();
int year, month, day, hour, minute, second;
if (n.getDigit4 (year) &&
n.getDigit2 (month) && month &&
n.getDigit2 (day) && day &&
n.skip ('T') &&
n.getDigit2 (hour) &&
n.getDigit2 (minute) && minute < 60 &&
n.getDigit2 (second) && second < 60)
{
if (n.skip ('Z'))
_utc = true;
_year = year;
_month = month;
_day = day;
_seconds = (((hour * 60) + minute) * 60) + second;
return true;
}
_year = 0;
_month = 0;
_day = 0;
_seconds = 0;
n.restore ();
return false;
}
////////////////////////////////////////////////////////////////////////////////
// date-ext 'T' time-ext 'Z'
// date-ext 'T' time-ext offset-ext
// date-ext 'T' time-ext
bool ISO8601d::parse_date_time_ext (Nibbler& n)
{
n.save ();
if (parse_date_ext (n))
{
if (n.skip ('T') &&
parse_time_ext (n))
{
if (n.skip ('Z'))
_utc = true;
else if (parse_off_ext (n))
;
if (! Lexer::isDigit (n.next ()))
return true;
}
// Restore date_ext
_year = 0;
_month = 0;
_week = 0;
_weekday = 0;
_julian = 0;
_day = 0;
}
n.restore ();
return false;
}
////////////////////////////////////////////////////////////////////////////////
// YYYY-MM-DD
// YYYY-DDD
// YYYY-Www-D
// YYYY-Www
bool ISO8601d::parse_date_ext (Nibbler& n)
{
Nibbler backup (n);
int year;
if (n.getDigit4 (year) &&
n.skip ('-'))
{
int month;
int day;
if (n.skip ('W') &&
n.getDigit2 (_week) && _week)
{
if (n.skip ('-') &&
n.getDigit (_weekday))
{
}
_year = year;
if (!Lexer::isDigit (n.next ()))
return true;
}
else if (n.getDigit3 (_julian) && _julian)
{
_year = year;
if (!Lexer::isDigit (n.next ()))
return true;
}
else if (n.getDigit2 (month) && month &&
n.skip ('-') &&
n.getDigit2 (day) && day)
{
_year = year;
_month = month;
_day = day;
if (!Lexer::isDigit (n.next ()))
return true;
}
}
n = backup;
return false;
}
////////////////////////////////////////////////////////////////////////////////
// ±hh[:mm]
bool ISO8601d::parse_off_ext (Nibbler& n)
{
Nibbler backup (n);
std::string sign;
if (n.getN (1, sign) && (sign == "+" || sign == "-"))
{
int offset;
int hh;
int mm;
if (n.getDigit2 (hh) && hh <= 12 &&
!n.getDigit (mm))
{
offset = hh * 3600;
if (n.skip (':'))
{
if (n.getDigit2 (mm) && mm < 60)
{
offset += mm * 60;
}
else
{
n = backup;
return false;
}
}
_offset = (sign == "-") ? -offset : offset;
if (!Lexer::isDigit (n.next ()))
return true;
}
}
n = backup;
return false;
}
////////////////////////////////////////////////////////////////////////////////
// hh:mm[:ss]
bool ISO8601d::parse_time_ext (Nibbler& n)
{
Nibbler backup (n);
int seconds = 0;
int hh;
int mm;
int ss;
if (n.getDigit2 (hh) && hh <= 24 &&
n.skip (':') &&
n.getDigit2 (mm) && mm < 60)
{
seconds = (hh * 3600) + (mm * 60);
if (n.skip (':'))
{
if (n.getDigit2 (ss) && ss < 60)
{
seconds += ss;
_seconds = seconds;
if (!Lexer::isDigit (n.next ()))
return true;
}
n = backup;
return false;
}
_seconds = seconds;
if (!Lexer::isDigit (n.next ()))
return true;
}
n = backup;
return false;
}
////////////////////////////////////////////////////////////////////////////////
// time-ext 'Z'
bool ISO8601d::parse_time_utc_ext (Nibbler& n)
{
n.save ();
if (parse_time_ext (n) &&
n.skip ('Z'))
{
_utc = true;
if (!Lexer::isDigit (n.next ()))
return true;
}
n.restore ();
return false;
}
////////////////////////////////////////////////////////////////////////////////
// time-ext offset-ext
bool ISO8601d::parse_time_off_ext (Nibbler& n)
{
Nibbler backup (n);
if (parse_time_ext (n) &&
parse_off_ext (n))
{
if (!Lexer::isDigit (n.next ()))
return true;
}
n = backup;
return false;
}
////////////////////////////////////////////////////////////////////////////////
// Using Zeller's Congruence.
int ISO8601d::dayOfWeek (int year, int month, int day)
{
int adj = (14 - month) / 12;
int m = month + 12 * adj - 2;
int y = year - adj;
return (day + (13 * m - 1) / 5 + y + y / 4 - y / 100 + y / 400) % 7;
}
////////////////////////////////////////////////////////////////////////////////
// Validation via simple range checking.
bool ISO8601d::validate ()
{
// _year;
if ((_year && (_year < 1900 || _year > 2200)) ||
(_month && (_month < 1 || _month > 12)) ||
(_week && (_week < 1 || _week > 53)) ||
(_weekday && (_weekday < 0 || _weekday > 6)) ||
(_julian && (_julian < 1 || _julian > Date::daysInYear (_year))) ||
(_day && (_day < 1 || _day > Date::daysInMonth (_month, _year))) ||
(_seconds && (_seconds < 1 || _seconds > 86400)) ||
(_offset && (_offset < -86400 || _offset > 86400)))
return false;
return true;
}
////////////////////////////////////////////////////////////////////////////////
// int tm_sec; seconds (0 - 60)
// int tm_min; minutes (0 - 59)
// int tm_hour; hours (0 - 23)
// int tm_mday; day of month (1 - 31)
// int tm_mon; month of year (0 - 11)
// int tm_year; year - 1900
// int tm_wday; day of week (Sunday = 0)
// int tm_yday; day of year (0 - 365)
// int tm_isdst; is summer time in effect?
// char *tm_zone; abbreviation of timezone name
// long tm_gmtoff; offset from UTC in seconds
void ISO8601d::resolve ()
{
// Don't touch the original values.
int year = _year;
int month = _month;
int week = _week;
int weekday = _weekday;
int julian = _julian;
int day = _day;
int seconds = _seconds;
int offset = _offset;
bool utc = _utc;
// Get current time.
time_t now = time (NULL);
// A UTC offset needs to be accommodated. Once the offset is subtracted,
// only local and UTC times remain.
if (offset)
{
seconds -= offset;
now -= offset;
utc = true;
}
// Get 'now' in the relevant location.
struct tm* t_now = utc ? gmtime (&now) : localtime (&now);
int seconds_now = (t_now->tm_hour * 3600) +
(t_now->tm_min * 60) +
t_now->tm_sec;
// Project forward one day if the specified seconds are earlier in the day
// than the current seconds.
if (year == 0 &&
month == 0 &&
day == 0 &&
week == 0 &&
weekday == 0 &&
seconds < seconds_now)
{
seconds += 86400;
}
// Convert week + weekday --> julian.
if (week)
{
julian = (week * 7) + weekday - dayOfWeek (year, 1, 4) - 3;
}
// Provide default values for year, month, day.
else
{
// Default values for year, month, day:
//
// y m d --> y m d
// y m - --> y m 1
// y - - --> y 1 1
// - - - --> now now now
//
if (year == 0)
{
year = t_now->tm_year + 1900;
month = t_now->tm_mon + 1;
day = t_now->tm_mday;
}
else
{
if (month == 0)
{
month = 1;
day = 1;
}
else if (day == 0)
day = 1;
}
}
if (julian)
{
month = 1;
day = julian;
}
struct tm t = {0};
t.tm_isdst = -1; // Requests that mktime/gmtime determine summer time effect.
t.tm_year = year - 1900;
t.tm_mon = month - 1;
t.tm_mday = day;
if (seconds > 86400)
{
int days = seconds / 86400;
t.tm_mday += days;
seconds %= 86400;
}
t.tm_hour = seconds / 3600;
t.tm_min = (seconds % 3600) / 60;
t.tm_sec = seconds % 60;
_value = utc ? timegm (&t) : mktime (&t);
}
////////////////////////////////////////////////////////////////////////////////
ISO8601p::ISO8601p ()
{
clear ();
}
////////////////////////////////////////////////////////////////////////////////
ISO8601p::ISO8601p (time_t input)
{
clear ();
_value = input;
}
////////////////////////////////////////////////////////////////////////////////
ISO8601p::ISO8601p (const std::string& input)
{
clear ();
if (Lexer::isAllDigits (input))
{
time_t value = (time_t) strtol (input.c_str (), NULL, 10);
if (value == 0 || value > 60)
{
_value = value;
return;
}
}
std::string::size_type idx = 0;
parse (input, idx);
}
////////////////////////////////////////////////////////////////////////////////
ISO8601p::~ISO8601p ()
{
}
////////////////////////////////////////////////////////////////////////////////
ISO8601p& ISO8601p::operator= (const ISO8601p& other)
{
if (this != &other)
{
_year = other._year;
_month = other._month;
_day = other._day;
_hours = other._hours;
_minutes = other._minutes;
_seconds = other._seconds;
_value = other._value;
}
return *this;
}
////////////////////////////////////////////////////////////////////////////////
bool ISO8601p::operator< (const ISO8601p& other)
{
return _value < other._value;
}
////////////////////////////////////////////////////////////////////////////////
bool ISO8601p::operator> (const ISO8601p& other)
{
return _value > other._value;
}
////////////////////////////////////////////////////////////////////////////////
ISO8601p::operator std::string () const
{
std::stringstream s;
s << _value;
return s.str ();
}
////////////////////////////////////////////////////////////////////////////////
ISO8601p::operator time_t () const
{
return _value;
}
////////////////////////////////////////////////////////////////////////////////
// Supported:
//
// duration ::= designated # duration
//
// designated ::= 'P' [nn 'Y'] [nn 'M'] [nn 'D'] ['T' [nn 'H'] [nn 'M'] [nn 'S']]
//
// Not supported:
//
// duration ::= designated '/' datetime-ext # duration end
// | degignated '/' datetime # duration end
// | designated # duration
// | 'P' datetime-ext '/' datetime-ext # start end
// | 'P' datetime '/' datetime # start end
// | 'P' datetime-ext # start
// | 'P' datetime # start
// | datetime-ext '/' designated # start duration
// | datetime-ext '/' 'P' datetime-ext # start end
// | datetime-ext '/' datetime-ext # start end
// | datetime '/' designated # start duration
// | datetime '/' 'P' datetime # start end
// | datetime '/' datetime # start end
// ;
//
bool ISO8601p::parse (const std::string& input, std::string::size_type& start)
{
// Attempt and ISO parse first.
auto original_start = start;
Nibbler n (input.substr (original_start));
n.save ();
if (parse_designated (n))
{
// Check the values and determine time_t.
if (validate ())
{
// Record cursor position.
start = n.cursor ();
resolve ();
return true;
}
}
// Attempt a legacy format parse next.
n.restore ();
// Static and so preserved between calls.
static std::vector <std::string> units;
if (units.size () == 0)
for (unsigned int i = 0; i < NUM_DURATIONS; i++)
units.push_back (durations[i].unit);
std::string number;
std::string unit;
if (n.getOneOf (units, unit))
{
if (n.depleted () ||
Lexer::isWhitespace (n.next ()) ||
Lexer::isSingleCharOperator (n.next ()))
{
start = original_start + n.cursor ();
// Linear lookup - should instead be logarithmic.
for (unsigned int i = 0; i < NUM_DURATIONS; i++)
{
if (durations[i].unit == unit &&
durations[i].standalone == true)
{
_value = static_cast <int> (durations[i].seconds);
return true;
}
}
}
}
else if (n.getNumber (number) &&
number.find ('e') == std::string::npos &&
number.find ('E') == std::string::npos &&
(number.find ('+') == std::string::npos || number.find ('+') == 0) &&
(number.find ('-') == std::string::npos || number.find ('-') == 0))
{
n.skipWS ();
if (n.getOneOf (units, unit))
{
// The "d" unit is a special case, because it is the only one that can
// legitimately occur at the beginning of a UUID, and be followed by an
// operator:
//
// 1111111d-0000-0000-0000-000000000000
//
// Because Lexer::isDuration is higher precedence than Lexer::isUUID,
// the above UUID looks like:
//
// <1111111d> <-> ...
// duration op ...
//
// So as a special case, durations, with units of "d" are rejected if the
// quantity exceeds 10000.
//
if (unit == "d" &&
strtol (number.c_str (), NULL, 10) > 10000)
return false;
if (n.depleted () ||
Lexer::isWhitespace (n.next ()) ||
Lexer::isSingleCharOperator (n.next ()))
{
start = original_start + n.cursor ();
double quantity = strtod (number.c_str (), NULL);
// Linear lookup - should instead be logarithmic.
double seconds = 1;
for (unsigned int i = 0; i < NUM_DURATIONS; i++)
{
if (durations[i].unit == unit)
{
seconds = durations[i].seconds;
_value = static_cast <int> (quantity * static_cast <double> (seconds));
return true;
}
}
}
}
}
return false;
}
////////////////////////////////////////////////////////////////////////////////
void ISO8601p::clear ()
{
_year = 0;
_month = 0;
_day = 0;
_hours = 0;
_minutes = 0;
_seconds = 0;
_value = 0;
}
////////////////////////////////////////////////////////////////////////////////
const std::string ISO8601p::format () const
{
if (_value)
{
time_t t = _value;
int seconds = t % 60; t /= 60;
int minutes = t % 60; t /= 60;
int hours = t % 24; t /= 24;
int days = t;
std::stringstream s;
s << 'P';
if (days) s << days << 'D';
if (hours || minutes || seconds)
{
s << 'T';
if (hours) s << hours << 'H';
if (minutes) s << minutes << 'M';
if (seconds) s << seconds << 'S';
}
return s.str ();
}
else
{
return "PT0S";
}
}
////////////////////////////////////////////////////////////////////////////////
// 'P' [nn 'Y'] [nn 'M'] [nn 'D'] ['T' [nn 'H'] [nn 'M'] [nn 'S']]
bool ISO8601p::parse_designated (Nibbler& n)
{
Nibbler backup (n);
if (n.skip ('P'))
{
int value;
n.save ();
if (n.getUnsignedInt (value) && n.skip ('Y'))
_year = value;
else
n.restore ();
n.save ();
if (n.getUnsignedInt (value) && n.skip ('M'))
_month = value;
else
n.restore ();
n.save ();
if (n.getUnsignedInt (value) && n.skip ('D'))
_day = value;
else
n.restore ();
if (n.skip ('T'))
{
n.save ();
if (n.getUnsignedInt (value) && n.skip ('H'))
_hours = value;
else
n.restore ();
n.save ();
if (n.getUnsignedInt (value) && n.skip ('M'))
_minutes = value;
else
n.restore ();
n.save ();
if (n.getUnsignedInt (value) && n.skip ('S'))
_seconds = value;
else
n.restore ();
}
return true;
}
n = backup;
return false;
}
////////////////////////////////////////////////////////////////////////////////
bool ISO8601p::validate ()
{
return _year ||
_month ||
_day ||
_hours ||
_minutes ||
_seconds;
}
////////////////////////////////////////////////////////////////////////////////
// Allow un-normalized values.
void ISO8601p::resolve ()
{
_value = (_year * 365 * 86400) +
(_month * 30 * 86400) +
(_day * 86400) +
(_hours * 3600) +
(_minutes * 60) +
_seconds;
}
////////////////////////////////////////////////////////////////////////////////