blob: 404c831392b6093d8219d22e038d19ce4262ea02 [file] [log] [blame]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
#include <boost/date_time/gregorian/gregorian.hpp>
#include <boost/date_time/posix_time/posix_time.hpp>
#include "cctz/civil_time.h"
#include "common/status.h"
#include "runtime/datetime-simple-date-format-parser.h"
#include "runtime/date-value.h"
#include "runtime/raw-value.inline.h"
#include "runtime/timestamp-value.h"
#include "testutil/gtest-util.h"
#include "common/names.h"
using boost::gregorian::date;
using boost::posix_time::time_duration;
namespace impala {
using namespace datetime_parse_util;
inline void ValidateDate(const DateValue& dv, int exp_year, int exp_month, int exp_day,
const string& desc) {
int year, month, day;
EXPECT_TRUE(dv.ToYearMonthDay(&year, &month, &day)) << desc;
EXPECT_EQ(exp_year, year);
EXPECT_EQ(exp_month, month);
EXPECT_EQ(exp_day, day);
}
inline DateValue ParseValidateDate(const char* s, bool accept_time_toks, int exp_year,
int exp_month, int exp_day) {
DCHECK(s != nullptr);
DateValue v = DateValue::ParseSimpleDateFormat(s, strlen(s), accept_time_toks);
ValidateDate(v, exp_year, exp_month, exp_day, s);
return v;
}
TEST(DateTest, ParseDefault) {
// Parse with time tokens rejected.
const DateValue v1 = ParseValidateDate("2012-01-20", false, 2012, 1, 20);
const DateValue v2 = ParseValidateDate("1990-10-20", false, 1990, 10, 20);
const DateValue v3 = ParseValidateDate("1990-10-20", false, 1990, 10, 20);
// Parse with time tokens accepted.
const DateValue v4 = ParseValidateDate("1990-10-20 23:59:59.999999999", true, 1990, 10,
20);
const DateValue v5 = ParseValidateDate("1990-10-20 00:01:02.9", true, 1990, 10, 20);
// Test comparison operators.
EXPECT_NE(v1, v2);
EXPECT_EQ(v2, v3);
EXPECT_LT(v2, v1);
EXPECT_LE(v2, v1);
EXPECT_GT(v1, v2);
EXPECT_GE(v2, v3);
// Time components are not part of the date value
EXPECT_EQ(v3, v4);
EXPECT_EQ(v3, v5);
EXPECT_NE(RawValue::GetHashValue(&v1, TYPE_DATE, 0),
RawValue::GetHashValue(&v2, TYPE_DATE, 0));
EXPECT_EQ(RawValue::GetHashValue(&v3, TYPE_DATE, 0),
RawValue::GetHashValue(&v2, TYPE_DATE, 0));
// 1-digit months and days are ok in date string.
ParseValidateDate("2012-1-20", false, 2012, 1, 20);
ParseValidateDate("2012-9-8", false, 2012, 9, 8);
// 1-digit hours/minutes/seconds are ok if time components are accepted.
ParseValidateDate("2012-09-8 01:1:2.9", true, 2012, 9, 8);
ParseValidateDate("2012-9-8 1:01:02", true, 2012, 9, 8);
// Different fractional seconds are accepted
ParseValidateDate("2012-09-8 01:01:2", true, 2012, 9, 8);
ParseValidateDate("2012-09-8 01:01:2.9", true, 2012, 9, 8);
ParseValidateDate("2012-09-8 01:01:02.9", true, 2012, 9, 8);
ParseValidateDate("2012-09-8 01:01:2.999", true, 2012, 9, 8);
ParseValidateDate("2012-09-8 01:01:02.999", true, 2012, 9, 8);
ParseValidateDate("2012-09-8 01:01:2.999999999", true, 2012, 9, 8);
ParseValidateDate("2012-09-8 01:01:02.999999999", true, 2012, 9, 8);
// Bad formats: invalid date component.
for (const char* s: {"1990-10", "1991-10-32", "1990-10-", "10:11:12 1991-10-10",
"02011-01-01", "999-01-01", "2012-01-200", "2011-001-01"}) {
EXPECT_FALSE(DateValue::ParseSimpleDateFormat(s, strlen(s), false).IsValid()) << s;
}
// Bad formats: valid date and time components but time component is rejected.
for (const char* s: {"2012-01-20 10:11:12", "2012-1-2 10:11:12"}) {
EXPECT_FALSE(DateValue::ParseSimpleDateFormat(s, strlen(s), false).IsValid()) << s;
}
// Bad formats: valid date component, invalid time component.
for (const char* s: {"2012-01-20 10:11:", "2012-1-2 10::12", "2012-01-20 :11:12",
"2012-01-20 24:11:12", "2012-01-20 23:60:12"}) {
EXPECT_FALSE(DateValue::ParseSimpleDateFormat(s, strlen(s), true).IsValid()) << s;
}
// Bad formats: missing date component, valid time component.
for (const char* s: {"10:11:12", "1:11:12", "10:1:12", "10:1:2.999"}) {
EXPECT_FALSE(DateValue::ParseSimpleDateFormat(s, strlen(s), true).IsValid()) << s;
}
}
// Used to represent a parsed date token. For example, it may represent a year.
struct DateToken {
const char* fmt;
int val;
const char* month_name;
DateToken(const char* fmt, int val)
: fmt(fmt),
val(val),
month_name(nullptr) {
}
DateToken(const char* month_fmt, int month_val, const char* month_name)
: fmt(month_fmt),
val(month_val),
month_name(month_name) {
}
friend bool operator<(const DateToken& lhs, const DateToken& rhs) {
return strcmp(lhs.fmt, rhs.fmt) < 0;
}
};
void TestDateTokens(const vector<DateToken>& toks, int year, int month, int day,
const char* separator) {
string fmt, val;
for (int i = 0; i < toks.size(); ++i) {
fmt.append(toks[i].fmt);
if (separator != nullptr && i + 1 < toks.size()) fmt.push_back(*separator);
if (toks[i].month_name != nullptr) {
val.append(string(toks[i].month_name));
} else {
val.append(lexical_cast<string>(toks[i].val));
}
if (separator != nullptr && i + 1 < toks.size()) val.push_back(*separator);
}
string fmt_val = "Format: " + fmt + ", Val: " + val;
DateTimeFormatContext dt_ctx(fmt.c_str());
ASSERT_TRUE(SimpleDateFormatTokenizer::Tokenize(&dt_ctx, false)) << fmt_val;
DateValue dv = DateValue::ParseSimpleDateFormat(val.c_str(), val.length(), dt_ctx);
ValidateDate(dv, year, month, day, fmt_val);
string buff = dv.Format(dt_ctx);
EXPECT_TRUE(!buff.empty()) << fmt_val;
EXPECT_LE(buff.length(), dt_ctx.fmt_len) << fmt_val;
EXPECT_EQ(buff, val) << fmt_val << " " << buff;
}
// This function will generate all permutations of tokens to test that the parsing and
// formatting is correct (position of tokens should be irrelevant). Note that separators
// are also combined with EACH token permutation to get the widest coverage on formats.
// This forces out the parsing and format logic edge cases.
void TestDateTokenPermutations(vector<DateToken>* toks, int year, int month, int day) {
sort(toks->begin(), toks->end());
const char* SEPARATORS = " ~!@%^&*_+-:;|\\,./";
do {
// Validate we can parse date raw tokens (no separators)
TestDateTokens(*toks, year, month, day, nullptr);
// Validate we can parse date with separators
for (const char* separator = SEPARATORS; *separator != 0; ++separator) {
TestDateTokens(*toks, year, month, day, separator);
}
} while (next_permutation(toks->begin(), toks->end()));
}
TEST(DateTest, ParseFormatCustomFormats) {
// Test custom formats by generating all permutations of tokens to check parsing and
// formatting is behaving correctly (position of tokens should be irrelevant). Note
// that separators are also combined with EACH token permutation to get the widest
// coverage on formats.
const int YEAR = 2013;
const int MONTH = 10;
const int DAY = 14;
// Test parsing/formatting with numeric date tokens
vector<DateToken> dt_toks{
DateToken("dd", DAY),
DateToken("MM", MONTH),
DateToken("yyyy", YEAR)};
TestDateTokenPermutations(&dt_toks, YEAR, MONTH, DAY);
}
TEST(DateTest, ParseFormatLiteralMonths) {
// Test literal months
const char* months[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep",
"Oct", "Nov", "Dec"
};
// Test parsing/formatting of literal months (short)
const int MONTH_CNT = (sizeof(months) / sizeof(char**));
const int YEAR = 2013;
const int DAY = 14;
for (int i = 0; i < MONTH_CNT; ++i) {
// Test parsing/formatting with short literal months
vector<DateToken> dt_lm_toks{
DateToken("dd", DAY),
DateToken("MMM", i + 1, months[i]),
DateToken("yyyy", YEAR)};
TestDateTokenPermutations(&dt_lm_toks, YEAR, i + 1, DAY);
}
}
// Used for defining a custom date format test. The structure can be used to indicate
// whether the format or value is expected to fail. In a happy path test, the values for
// year, month, day will be validated against the parsed result.
// Further validation will also be performed if the should_format flag is enabled,
// whereby the parsed date will be translated back to a string and checked against the
// expected value.
struct DateTC {
const char* fmt;
const char* str;
bool fmt_should_fail;
bool str_should_fail;
bool should_format;
int expected_year;
int expected_month;
int expected_day;
DateTC(const char* fmt, const char* str, bool fmt_should_fail = true,
bool str_should_fail = true)
: fmt(fmt),
str(str),
fmt_should_fail(fmt_should_fail),
str_should_fail(str_should_fail),
should_format(true),
expected_year(0),
expected_month(0),
expected_day(0) {
}
DateTC(const char* fmt, const char* str, bool should_format,
int expected_year, int expected_month, int expected_day)
: fmt(fmt),
str(str),
fmt_should_fail(false),
str_should_fail(false),
should_format(should_format),
expected_year(expected_year),
expected_month(expected_month),
expected_day(expected_day) {
}
void Run(int id, const TimestampValue& now) const {
DateTimeFormatContext dt_ctx(fmt);
dt_ctx.SetCenturyBreakAndCurrentTime(now);
stringstream desc;
desc << "DateTC [" << id << "]: " << " fmt:" << fmt << " str:" << str
<< " expected date:" << expected_year << "/" << expected_month << "/"
<< expected_day;
bool parse_result = SimpleDateFormatTokenizer::Tokenize(&dt_ctx, false);
if (fmt_should_fail) {
EXPECT_FALSE(parse_result) << desc.str();
return;
} else {
ASSERT_TRUE(parse_result) << desc.str();
}
DateValue cust_dv = DateValue::ParseSimpleDateFormat(str, strlen(str), dt_ctx);
if (str_should_fail) {
EXPECT_FALSE(cust_dv.IsValid()) << desc.str();
return;
}
// Check the date (based on any date format tokens being present)
ValidateDate(cust_dv, expected_year, expected_month, expected_day, desc.str());
// Check formatted date
if (!should_format) return;
string buff = cust_dv.Format(dt_ctx);
EXPECT_TRUE(!buff.empty()) << desc.str();
EXPECT_LE(buff.length(), dt_ctx.fmt_len) << desc.str();
EXPECT_EQ(string(str, strlen(str)), buff) << desc.str();
}
};
TEST(DateTest, ParseFormatEdgeCases) {
const TimestampValue now(date(1980, 2, 28), time_duration(16, 14, 24));
vector<DateTC> test_cases{
// Test year lower/upper bound
DateTC("yyyy-MM-dd", "0001-01-01", true, 1, 1, 1),
DateTC("yyyy-MM-dd", "0000-01-01", false, true),
DateTC("yyyy-MM-dd", "-001-01-01", false, true),
DateTC("yyyy-MM-dd", "9999-12-31", true, 9999, 12, 31),
DateTC("yyyyy-MM-dd", "10000-12-31", false, true),
// Test Feb 29 in leap years
DateTC("yyyy-MM-dd", "0004-02-29", true, 4, 2, 29),
DateTC("yyyy-MM-dd", "1904-02-29", true, 1904, 2, 29),
DateTC("yyyy-MM-dd", "2000-02-29", true, 2000, 2, 29),
// Test Feb 29 in non-leap years
DateTC("yyyy-MM-dd", "0001-02-29", false, true),
DateTC("yyyy-MM-dd", "1900-02-29", false, true),
DateTC("yyyy-MM-dd", "1999-02-29", false, true)};
for (int i = 0; i < test_cases.size(); ++i) {
test_cases[i].Run(i, now);
}
}
TEST(DateTest, ParseFormatSmallYear) {
// Fix current time to determine the behavior parsing 2-digit year format.
const TimestampValue now(date(1980, 2, 28), time_duration(16, 14, 24));
// Test year < 1000
vector<DateTC> test_cases{
DateTC("yyyy-MM-dd", "0999-10-31", true, 999, 10, 31),
DateTC("yyyy-MM-dd", "0099-10-31", true, 99, 10, 31),
DateTC("yyyy-MM-dd", "0009-10-31", true, 9, 10, 31),
// Format token yyy works when parsing years < 1000.
// On the other hand when yyy is used for formatting years, modulo 100 will be
// applied
DateTC("yyy-MM-dd", "999-10-31", false, 999, 10, 31),
DateTC("yyy-MM-dd", "099-10-31", true, 99, 10, 31),
DateTC("yyy-MM-dd", "009-10-31", true, 9, 10, 31),
// Year is aligned when yy format token is used and we have a 2-difgit year. 3-digit
// years are not parsed correctly.
DateTC("yy-MM-dd", "999-10-31", false, true),
DateTC("yy-MM-dd", "99-10-31", true, 1999, 10, 31),
DateTC("yy-MM-dd", "09-10-31", true, 1909, 10, 31),
// Year is aligned when y format token is used and we have a 2-digit year.
DateTC("y-MM-dd", "999-10-31", false, 999, 10, 31),
DateTC("y-MM-dd", "99-10-31", false, 1999, 10, 31),
DateTC("y-MM-dd", "09-10-31", false, 1909, 10, 31),
DateTC("y-MM-dd", "9-10-31", false, 1909, 10, 31)};
for (int i = 0; i < test_cases.size(); ++i) {
test_cases[i].Run(i, now);
}
}
TEST(DateTest, ParseFormatAlignedYear) {
// Fix current time to determine the behavior parsing 2-digit year format.
// Set it to 02/28 to test 02/29 edge cases.
// The corresponding century break will be 1900-02-28.
const TimestampValue now(date(1980, 2, 28), time_duration(16, 14, 24));
// Test year alignment for 1- and 2-digit year format.
vector<DateTC> test_cases{
// Test 2-digit year format
DateTC("yy-MM-dd", "17-08-31", true, 1917, 8, 31),
DateTC("yy-MM-dd", "99-08-31", true, 1999, 8, 31),
// Test 02/29 edge cases of 2-digit year format
DateTC("yy-MM-dd", "00-02-28", true, 2000, 2, 28),
// After the cutoff year is 1900, but 1900/02/29 is invalid
DateTC("yy-MM-dd", "00-02-29", false, true),
// After the cutoff year is 1900
DateTC("yy-MM-dd", "00-03-01", true, 1900, 3, 1),
DateTC("yy-MM-dd", "04-02-29", true, 1904, 2, 29),
DateTC("yy-MM-dd", "99-02-29", false, true),
// Test 1-digit year format with time to show the exact boundary
// Before the cutoff, year should be 2000
DateTC("y-MM-dd", "00-02-28", false, 2000, 2, 28),
// After the cutoff year is 1900, but 1900/02/29 is invalid
DateTC("y-MM-dd", "00-02-29", false, true),
// After the cutoff year is 1900.
DateTC("y-MM-dd", "00-03-01", false, 1900, 3, 1)};
for (int i = 0; i < test_cases.size(); ++i) {
test_cases[i].Run(i, now);
}
// Test year realignment with a different 'now' timestamp.
// This time the corresponding century break will be 1938-09-25.
const TimestampValue now2(date(2018, 9, 25), time_duration(16, 14, 24));
vector<DateTC> test_cases2{
// Before the cutoff, year is 2004.
DateTC("yy-MM-dd", "04-02-29", true, 2004, 2, 29),
// Still before the cutoff, year is 2038.
DateTC("yy-MM-dd", "38-09-25", true, 2038, 9, 25),
// After the cutoff, year is 1938.
DateTC("yy-MM-dd", "38-09-26", true, 1938, 9, 26),
// Test parsing again with 'y' format token.
DateTC("y-MM-dd", "04-02-29", false, 2004, 2, 29),
DateTC("y-MM-dd", "38-09-25", false, 2038, 9, 25),
DateTC("y-MM-dd", "38-09-26", false, 1938, 9, 26)};
for (int i = 0; i < test_cases2.size(); ++i) {
test_cases2[i].Run(i + test_cases.size(), now2);
}
}
TEST(DateTest, ParseFormatComplexFormats) {
const TimestampValue now(date(1980, 2, 28), time_duration(16, 14, 24));
// Test parsing/formatting of complex date formats
vector<DateTC> test_cases{
// Test case on literal short months
DateTC("yyyy-MMM-dd", "2013-OCT-01", false, 2013, 10, 1),
// Test case on literal short months
DateTC("yyyy-MMM-dd", "2013-oct-01", false, 2013, 10, 1),
// Test case on literal short months
DateTC("yyyy-MMM-dd", "2013-oCt-01", false, 2013, 10, 1),
// Test padding on numeric and literal tokens (short,
DateTC("MMMyyyyyydd", "Apr00201309", true, 2013, 4, 9),
// Test duplicate tokens
DateTC("yyyy MM dd ddMMMyyyy", "2013 05 12 16Apr1952", false, 1952, 4, 16),
// Test missing separator on short date format
DateTC("Myyd", "4139", true, true),
// Test bad year format
DateTC("YYYYmmdd", "20131001"),
// Test unknown formatting character
DateTC("yyyyUUdd", "2013001001"),
// Test that T|Z markers and time tokens are rejected
DateTC("yyyy-MM-ddT", "2013-11-12T"),
DateTC("yyyy-MM-ddZ", "2013-11-12Z"),
DateTC("yyyy-MM-dd HH:mm:ss", "2013-11-12 12:23:36"),
DateTC("HH:mm:ss", "12:23:36"),
// Test numeric formatting character
DateTC("yyyyMM1dd", "201301111"),
// Test out of range year
DateTC("yyyyyMMdd", "120130101", false, true),
// Test out of range month
DateTC("yyyyMMdd", "20131301", false, true),
// Test out of range month
DateTC("yyyyMMdd", "20130001", false, true),
// Test out of range day
DateTC("yyyyMMdd", "20130132", false, true),
// Test out of range day
DateTC("yyyyMMdd", "20130100", false, true),
// Test characters where numbers should be
DateTC("yyyyMMdd", "201301aa", false, true),
// Test missing year
DateTC("MMdd", "1201", false, true),
// Test missing month
DateTC("yyyydd", "201301", false, true),
DateTC("yydd", "1301", false, true),
// Test missing day
DateTC("yyyyMM", "201301", false, true),
DateTC("yyMM", "8512", false, true),
// Test missing month and day
DateTC("yyyy", "2013", false, true),
DateTC("yy", "13", false, true),
// Test short year token
DateTC("y-MM-dd", "2013-11-13", false, 2013, 11, 13),
DateTC("y-MM-dd", "13-11-13", false, 1913, 11, 13),
// Test short month token
DateTC("yyyy-M-dd", "2013-11-13", false, 2013, 11, 13),
DateTC("yyyy-M-dd", "2013-1-13", false, 2013, 1, 13),
// Test short day token
DateTC("yyyy-MM-d", "2013-11-13", false, 2013, 11, 13),
DateTC("yyyy-MM-d", "2013-11-3", false, 2013, 11, 3),
// Test short all date tokens
DateTC("y-M-d", "2013-11-13", false, 2013, 11, 13),
DateTC("y-M-d", "13-1-3", false, 1913, 1, 3)};
// Loop through custom parse/format test cases and execute each one. Each test case
// will be explicitly set with a pass/fail expectation related to either the format
// or literal value.
for (int i = 0; i < test_cases.size(); ++i) {
test_cases[i].Run(i, now);
}
}
// Used to test custom date output test cases i.e. date value -> string.
struct DateFormatTC {
const int32_t days_since_epoch;
const char* fmt;
const char* str;
bool should_fail;
DateFormatTC(int32_t days_since_epoch, const char* fmt, const char* str,
bool should_fail = false)
: days_since_epoch(days_since_epoch),
fmt(fmt),
str(str),
should_fail(should_fail) {
}
void Run(int id, const TimestampValue& now) const {
DateTimeFormatContext dt_ctx(fmt);
dt_ctx.SetCenturyBreakAndCurrentTime(now);
stringstream desc;
desc << "DateFormatTC [" << id << "]: " << "days_since_epoch:" << days_since_epoch
<< " fmt:" << fmt << " str:" << str;
ASSERT_TRUE(SimpleDateFormatTokenizer::Tokenize(&dt_ctx, false)) << desc.str();
const DateValue cust_dv(days_since_epoch);
EXPECT_TRUE(cust_dv.IsValid()) << desc.str();
EXPECT_GE(dt_ctx.fmt_out_len, dt_ctx.fmt_len) << desc.str();
string buff = cust_dv.Format(dt_ctx);
EXPECT_TRUE(!buff.empty()) << desc.str();
EXPECT_LE(buff.length(), dt_ctx.fmt_out_len) << desc.str();
EXPECT_EQ(buff, string(str, strlen(str))) << desc.str();
}
};
TEST(DateTest, FormatComplexFormats) {
const TimestampValue now(date(1980, 2, 28), time_duration(16, 14, 24));
// Test complex formatting of dates
vector<DateFormatTC> fmt_test_cases{
// Test just formatting date tokens
DateFormatTC(11178, "yyyy-MM-dd", "2000-08-09"),
// Test short form date tokens
DateFormatTC(11178, "yyyy-M-d", "2000-8-9"),
// Test short form tokens on wide dates
DateFormatTC(15999, "d", "21"),
// Test month expansion
DateFormatTC(11178, "MMM/MM/M", "Aug/08/8"),
// Test padding on single digits
DateFormatTC(11178, "dddddd/dd/d", "000009/09/9"),
// Test padding on double digits
DateFormatTC(15999, "dddddd/dd/dd", "000021/21/21")};
// Loop through format test cases
for (int i = 0; i < fmt_test_cases.size(); ++i) {
fmt_test_cases[i].Run(i, now);
}
}
TEST(DateTest, DateValueEdgeCases) {
// Test min supported date.
// MIN_DATE_DAYS_SINCE_EPOCH was calculated using the Proleptic Gregorian calendar. This
// is expected to be different then how Hive2 written Parquet files represent
// 0001-01-01.
const int32_t MIN_DATE_DAYS_SINCE_EPOCH = -719162;
const DateValue min_date1 = ParseValidateDate("0001-01-01", true, 1, 1, 1);
const DateValue min_date2 = ParseValidateDate("0001-01-01 00:00:00", true, 1, 1, 1);
EXPECT_EQ(min_date1, min_date2);
int32_t min_days;
EXPECT_TRUE(min_date1.ToDaysSinceEpoch(&min_days));
EXPECT_EQ(MIN_DATE_DAYS_SINCE_EPOCH, min_days);
EXPECT_EQ("0001-01-01", min_date1.ToString());
EXPECT_EQ("0001-01-01", min_date2.ToString());
const DateValue min_date3(MIN_DATE_DAYS_SINCE_EPOCH);
EXPECT_TRUE(min_date3.IsValid());
EXPECT_EQ(min_date1, min_date3);
const DateValue too_early(MIN_DATE_DAYS_SINCE_EPOCH - 1);
EXPECT_FALSE(too_early.IsValid());
// Test max supported date.
const int32_t MAX_DATE_DAYS_SINCE_EPOCH = 2932896;
const DateValue max_date1 = ParseValidateDate("9999-12-31", true, 9999, 12, 31);
const DateValue max_date2 = ParseValidateDate("9999-12-31 23:59:59.999999999", true,
9999, 12, 31);
EXPECT_EQ(max_date1, max_date2);
int32_t max_days;
EXPECT_TRUE(max_date1.ToDaysSinceEpoch(&max_days));
EXPECT_EQ(MAX_DATE_DAYS_SINCE_EPOCH, max_days);
EXPECT_EQ("9999-12-31", max_date1.ToString());
EXPECT_EQ("9999-12-31", max_date2.ToString());
const DateValue max_date3(MAX_DATE_DAYS_SINCE_EPOCH);
EXPECT_TRUE(max_date3.IsValid());
EXPECT_EQ(max_date1, max_date3);
const DateValue too_late(MAX_DATE_DAYS_SINCE_EPOCH + 1);
EXPECT_FALSE(too_late.IsValid());
// Test that Feb 29 is valid in leap years.
for (int leap_year: {4, 1904, 1980, 1996, 2000, 2004, 2104, 9996}) {
EXPECT_TRUE(DateValue(leap_year, 2, 29).IsValid()) << "year:" << leap_year;
}
// Test that Feb 29 is invalid in non-leap years.
for (int non_leap_year: {1, 1900, 1981, 1999, 2001, 2100, 9999}) {
EXPECT_TRUE(DateValue(non_leap_year, 2, 28).IsValid()) << "year:" << non_leap_year;
EXPECT_FALSE(DateValue(non_leap_year, 2, 29).IsValid()) << "year:" << non_leap_year;
EXPECT_TRUE(DateValue(non_leap_year, 3, 1).IsValid()) << "year:" << non_leap_year;
}
}
TEST(DateTest, AddDays) {
// Adding days to an invalid DateValue instance returns an invalid DateValue.
DateValue invalid_dv;
EXPECT_FALSE(invalid_dv.IsValid());
EXPECT_FALSE(invalid_dv.AddDays(1).IsValid());
// AddDays works with 0, > 0 and < 0 number of days.
DateValue dv(2019, 5, 16);
EXPECT_EQ(DateValue(2019, 5, 17), dv.AddDays(1));
EXPECT_EQ(DateValue(2019, 5, 15), dv.AddDays(-1));
// May has 31 days, April has 30 days.
EXPECT_EQ(DateValue(2019, 6, 16), dv.AddDays(31));
EXPECT_EQ(DateValue(2019, 4, 16), dv.AddDays(-30));
// 2019 is not a leap year, 2020 is a leap year.
EXPECT_EQ(DateValue(2020, 5, 16), dv.AddDays(366));
EXPECT_EQ(DateValue(2018, 5, 16), dv.AddDays(-365));
// Test upper limit.
dv = DateValue(9999, 12, 20);
EXPECT_EQ(DateValue(9999, 12, 31), dv.AddDays(11));
EXPECT_FALSE(dv.AddDays(12).IsValid());
EXPECT_FALSE(dv.AddDays(13).IsValid());
// Test lower limit.
dv = DateValue(1, 1, 10);
EXPECT_EQ(DateValue(1, 1, 1), dv.AddDays(-9));
EXPECT_FALSE(dv.AddDays(-10).IsValid());
EXPECT_FALSE(dv.AddDays(-11).IsValid());
// Test adding days to cover the entire range.
int32_t min_dse, max_dse;
EXPECT_TRUE(DateValue(1, 1, 1).ToDaysSinceEpoch(&min_dse));
EXPECT_GT(0, min_dse);
min_dse = -min_dse;
EXPECT_TRUE(DateValue(9999, 12, 31).ToDaysSinceEpoch(&max_dse));
EXPECT_LT(0, max_dse);
dv = DateValue(1, 1, 1);
EXPECT_EQ(DateValue(9999, 12, 31), dv.AddDays(min_dse + max_dse));
EXPECT_FALSE(dv.AddDays(min_dse + max_dse + 1).IsValid());
EXPECT_FALSE(dv.AddDays(std::numeric_limits<int64_t>::max()).IsValid());
dv = DateValue(9999, 12, 31);
EXPECT_EQ(DateValue(1, 1, 1), dv.AddDays(-(min_dse + max_dse)));
EXPECT_FALSE(dv.AddDays(-(min_dse + max_dse + 1)).IsValid());
EXPECT_FALSE(dv.AddDays(std::numeric_limits<int64_t>::min()).IsValid());
// Test leap year.
dv = DateValue(2000, 2, 20);
EXPECT_EQ(DateValue(2000, 2, 28), dv.AddDays(8));
EXPECT_EQ(DateValue(2000, 2, 29), dv.AddDays(9));
EXPECT_EQ(DateValue(2000, 3, 1), dv.AddDays(10));
// Test non-leap year.
dv = DateValue(2001, 2, 20);
EXPECT_EQ(DateValue(2001, 2, 28), dv.AddDays(8));
EXPECT_EQ(DateValue(2001, 3, 1), dv.AddDays(9));
}
TEST(DateTest, AddMonths) {
// Adding days to an invalid DateValue instance returns an invalid DateValue.
DateValue invalid_dv;
EXPECT_FALSE(invalid_dv.IsValid());
EXPECT_FALSE(invalid_dv.AddMonths(1, true).IsValid());
// AddMonths works with 0, > 0 and < 0 number of months.
DateValue dv(2019, 5, 16);
EXPECT_EQ(DateValue(2019, 6, 16), dv.AddMonths(1, true));
EXPECT_EQ(DateValue(2019, 4, 16), dv.AddMonths(-1, true));
// Test that result dates are always capped at the end of the month regardless of
// whether 'keep_last_day' is set or not.
dv = DateValue(2019, 5, 31);
EXPECT_EQ(DateValue(2019, 6, 30), dv.AddMonths(1, true));
EXPECT_EQ(DateValue(2019, 7, 31), dv.AddMonths(2, true));
EXPECT_EQ(DateValue(2020, 2, 29), dv.AddMonths(9, true));
EXPECT_EQ(DateValue(2019, 6, 30), dv.AddMonths(1, false));
EXPECT_EQ(DateValue(2019, 7, 31), dv.AddMonths(2, false));
EXPECT_EQ(DateValue(2020, 2, 29), dv.AddMonths(9, false));
// Test that resulting date falls on the last day iff 'keep_last_day' is set.
dv = DateValue(1999, 2, 28);
EXPECT_EQ(DateValue(1999, 3, 31), dv.AddMonths(1, true));
EXPECT_EQ(DateValue(1999, 4, 30), dv.AddMonths(2, true));
EXPECT_EQ(DateValue(2000, 2, 29), dv.AddMonths(12, true));
EXPECT_EQ(DateValue(1999, 3, 28), dv.AddMonths(1, false));
EXPECT_EQ(DateValue(1999, 4, 28), dv.AddMonths(2, false));
EXPECT_EQ(DateValue(2000, 2, 28), dv.AddMonths(12, false));
// Test that leap year is handled correctly.
dv = DateValue(2016, 2, 29);
EXPECT_EQ(DateValue(2016, 3, 31), dv.AddMonths(1, true));
EXPECT_EQ(DateValue(2016, 4, 30), dv.AddMonths(2, true));
EXPECT_EQ(DateValue(2017, 2, 28), dv.AddMonths(12, true));
EXPECT_EQ(DateValue(2016, 3, 29), dv.AddMonths(1, false));
EXPECT_EQ(DateValue(2016, 4, 29), dv.AddMonths(2, false));
EXPECT_EQ(DateValue(2017, 2, 28), dv.AddMonths(12, false));
// Test upper limit.
dv = DateValue(9998, 11, 30);
EXPECT_EQ(DateValue(9999, 12, 31), dv.AddMonths(13, true));
EXPECT_FALSE(dv.AddMonths(14, true).IsValid());
// Test lower limit.
dv = DateValue(1, 11, 30);
EXPECT_EQ(DateValue(1, 1, 31), dv.AddMonths(-10, true));
EXPECT_FALSE(dv.AddMonths(-11, true).IsValid());
// Test adding months to cover the entire range.
dv = DateValue(1, 1, 1);
EXPECT_EQ(DateValue(9999, 12, 1), dv.AddMonths(9998 * 12 + 11, false));
EXPECT_FALSE(dv.AddMonths(9998 * 12 + 12, false).IsValid());
EXPECT_FALSE(dv.AddMonths(std::numeric_limits<int64_t>::max(), false).IsValid());
dv = DateValue(9999, 12, 31);
EXPECT_EQ(DateValue(1, 1, 31), dv.AddMonths(-9998 * 12 - 11, false));
EXPECT_FALSE(dv.AddMonths(-9998 * 12 - 12, false).IsValid());
EXPECT_FALSE(dv.AddMonths(std::numeric_limits<int64_t>::min(), false).IsValid());
}
TEST(DateTest, AddYears) {
// Adding years to an invalid DateValue instance returns an invalid DateValue.
DateValue invalid_dv;
EXPECT_FALSE(invalid_dv.IsValid());
EXPECT_FALSE(invalid_dv.AddYears(1).IsValid());
// AddYears works with 0, > 0 and < 0 number of days.
DateValue dv(2019, 5, 16);
EXPECT_EQ(DateValue(2020, 5, 16), dv.AddYears(1));
EXPECT_EQ(DateValue(2018, 5, 16), dv.AddYears(-1));
// Test upper limit.
dv = DateValue(9990, 12, 31);
EXPECT_EQ(DateValue(9999, 12, 31), dv.AddYears(9));
EXPECT_FALSE(dv.AddYears(10).IsValid());
EXPECT_FALSE(dv.AddYears(11).IsValid());
// Test lower limit.
dv = DateValue(11, 1, 1);
EXPECT_EQ(DateValue(1, 1, 1), dv.AddYears(-10));
EXPECT_FALSE(dv.AddYears(-11).IsValid());
EXPECT_FALSE(dv.AddYears(-12).IsValid());
// Test adding years to cover the entire range.
dv = DateValue(1, 1, 1);
EXPECT_EQ(DateValue(9999, 1, 1), dv.AddYears(9998));
EXPECT_FALSE(dv.AddYears(9998 + 1).IsValid());
EXPECT_FALSE(dv.AddYears(std::numeric_limits<int64_t>::max()).IsValid());
dv = DateValue(9999, 12, 31);
EXPECT_EQ(DateValue(1, 12, 31), dv.AddYears(-9998));
EXPECT_FALSE(dv.AddYears(-9998 - 1).IsValid());
EXPECT_FALSE(dv.AddYears(std::numeric_limits<int64_t>::min()).IsValid());
// Test leap year.
dv = DateValue(2000, 2, 29);
EXPECT_EQ(DateValue(2001, 2, 28), dv.AddYears(1));
EXPECT_EQ(DateValue(2002, 2, 28), dv.AddYears(2));
EXPECT_EQ(DateValue(2003, 2, 28), dv.AddYears(3));
EXPECT_EQ(DateValue(2004, 2, 29), dv.AddYears(4));
}
TEST(DateTest, WeekDay) {
// WeekDay() returns -1 for invalid dates.
DateValue invalid_dv;
EXPECT_FALSE(invalid_dv.IsValid());
EXPECT_EQ(-1, invalid_dv.WeekDay());
// 2019-05-01 is Wednesday.
DateValue dv(2019, 5, 1);
for (int i = 0; i <= 31; ++i) {
// 0 = Monday, 2 = Wednesday and 6 = Sunday.
EXPECT_EQ((i + 2) % 7, dv.AddDays(i).WeekDay());
}
// Test upper limit. 9999-12-31 is Friday.
EXPECT_EQ(4, DateValue(9999, 12, 31).WeekDay());
// Test lower limit.
// 0001-01-01 is Monday.
EXPECT_EQ(0, DateValue(1, 1, 1).WeekDay());
// 0002-01-01 is Tuesday.
EXPECT_EQ(1, DateValue(2, 1, 1).WeekDay());
}
TEST(DateTest, ToYearMonthDay) {
// Test that ToYearMonthDay() and ToYear() return false for invalid dates.
DateValue invalid_dv;
EXPECT_FALSE(invalid_dv.IsValid());
int y1, m1, d1;
EXPECT_FALSE(invalid_dv.ToYearMonthDay(&y1, &m1, &d1));
int y2;
EXPECT_FALSE(invalid_dv.ToYear(&y2));
// Test that ToYearMonthDay() and ToYear() return the same values as
// cctz::civil_day::year()/month()/day().
// The following loop iterates through all valid dates (0001-01-01..9999-12-31):
cctz::civil_day epoch(1970, 1, 1);
cctz::civil_day cd(1, 1, 1);
do {
DateValue dv(cd - epoch);
EXPECT_TRUE(dv.IsValid());
EXPECT_TRUE(dv.ToYearMonthDay(&y1, &m1, &d1));
EXPECT_EQ(cd.year(), y1);
EXPECT_EQ(cd.month(), m1);
EXPECT_EQ(cd.day(), d1);
EXPECT_TRUE(dv.ToYear(&y2));
EXPECT_EQ(cd.year(), y2);
cd++;
} while (cd.year() < 10000);
}
TEST(DateTest, DayOfYear) {
DateValue invalid_dv;
EXPECT_EQ(-1, invalid_dv.DayOfYear());
// Test lower limit.
EXPECT_EQ(1, DateValue(1, 1, 1).DayOfYear());
// Test upper limit.
EXPECT_EQ(365, DateValue(9999, 12,31).DayOfYear());
// Test leap year.
EXPECT_EQ(1, DateValue(2000, 1, 1).DayOfYear());
EXPECT_EQ(31, DateValue(2000, 1, 31).DayOfYear());
EXPECT_EQ(32, DateValue(2000, 2, 1).DayOfYear());
EXPECT_EQ(59, DateValue(2000, 2, 28).DayOfYear());
EXPECT_EQ(60, DateValue(2000, 2, 29).DayOfYear());
EXPECT_EQ(61, DateValue(2000, 3, 1).DayOfYear());
EXPECT_EQ(366, DateValue(2000, 12, 31).DayOfYear());
}
TEST(DateTest, WeekOfYear) {
// Test that it returns -1 for invalid dates.
DateValue invalid_dv;
EXPECT_EQ(-1, invalid_dv.WeekOfYear());
// Iterate through days of 2019.
// 2019-01-01 is Tuesday and 2019-12-31 is Tuesday too.
DateValue jan1(2019, 1, 1);
int weekday_offset = 1;
for (DateValue dv = jan1;
dv <= DateValue(2019, 12, 29);
dv = dv.AddDays(1)) {
EXPECT_EQ(weekday_offset / 7 + 1, dv.WeekOfYear());
++weekday_offset;
}
// Year 2015 has 53 weeks. 2015-12-31 is Thursday.
EXPECT_EQ(53, DateValue(2015, 12, 31).WeekOfYear());
// 2019-12-30 (Monday) and 2019-12-31 (Tuesday) belong to year 2020.
EXPECT_EQ(1, DateValue(2019, 12, 30).WeekOfYear());
EXPECT_EQ(1, DateValue(2019, 12, 31).WeekOfYear());
EXPECT_EQ(1, DateValue(2020, 1, 1).WeekOfYear());
EXPECT_EQ(1, DateValue(2020, 1, 5).WeekOfYear());
EXPECT_EQ(2, DateValue(2020, 1, 6).WeekOfYear());
// 0002-01-01 is Tuesday. Test days around 0002-01-01.
EXPECT_EQ(51, DateValue(1, 12, 23).WeekOfYear());
EXPECT_EQ(52, DateValue(1, 12, 30).WeekOfYear());
EXPECT_EQ(1, DateValue(1, 12, 31).WeekOfYear());
EXPECT_EQ(1, DateValue(2, 1, 1).WeekOfYear());
EXPECT_EQ(1, DateValue(2, 1, 6).WeekOfYear());
EXPECT_EQ(2, DateValue(2, 1, 7).WeekOfYear());
// 0001-01-01 is Monday. Test days around 0001-01-01.
EXPECT_EQ(1, DateValue(1, 1, 1).WeekOfYear());
EXPECT_EQ(1, DateValue(1, 1, 2).WeekOfYear());
EXPECT_EQ(2, DateValue(1, 1, 8).WeekOfYear());
// 9999-12-31 is Friday. Test days around 9999-12-31.
EXPECT_EQ(52, DateValue(9999, 12, 31).WeekOfYear());
EXPECT_EQ(52, DateValue(9999, 12, 27).WeekOfYear());
EXPECT_EQ(51, DateValue(9999, 12, 26).WeekOfYear());
}
TEST(DateTest, LastDay) {
// Test that it returns invalid DateValue for invalid dates.
DateValue invalid_dv;
EXPECT_FALSE(invalid_dv.LastDay().IsValid());
// Test a non-leap year.
int month_days[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
for (DateValue dv(2019, 1, 1); dv <= DateValue(2019, 12, 31); dv = dv.AddDays(1)) {
int year, month, day;
EXPECT_TRUE(dv.ToYearMonthDay(&year, &month, &day));
EXPECT_EQ(DateValue(year, month, month_days[month - 1]), dv.LastDay());
}
// Test a leap year.
month_days[1] = 29;
for (DateValue dv(2016, 1, 1); dv <= DateValue(2016, 12, 31); dv = dv.AddDays(1)) {
int year, month, day;
EXPECT_TRUE(dv.ToYearMonthDay(&year, &month, &day));
EXPECT_EQ(DateValue(year, month, month_days[month - 1]), dv.LastDay());
}
// Test upper limit.
EXPECT_EQ(DateValue(9999, 12, 31), DateValue(9999, 12, 1).LastDay());
EXPECT_EQ(DateValue(9999, 12, 31), DateValue(9999, 12, 31).LastDay());
// Test lower limit.
EXPECT_EQ(DateValue(1, 1, 31), DateValue(1, 1, 1).LastDay());
EXPECT_EQ(DateValue(1, 1, 31), DateValue(1, 1, 31).LastDay());
}
// These macros add scoped trace to provide the line number of the caller upon failure.
#define TEST_MONTHS_BW_RANGE(date1, date2, min_expected, max_expected) { \
SCOPED_TRACE(""); \
TestMonthsBetween((date1), (date2), (min_expected), (max_expected)); \
}
#define TEST_MONTHS_BW(date1, date2, expected) { \
SCOPED_TRACE(""); \
TestMonthsBetween((date1), (date2), (expected), (expected)); \
}
void TestMonthsBetween(const DateValue& dv1, const DateValue& dv2, double min_expected,
double max_expected) {
double months_between;
EXPECT_TRUE(dv1.MonthsBetween(dv2, &months_between));
EXPECT_LE(min_expected, months_between);
EXPECT_LE(months_between, max_expected);
}
TEST(DateTest, MonthsBetween) {
DateValue invalid_dv;
double months_between;
EXPECT_FALSE(invalid_dv.MonthsBetween(DateValue(), &months_between));
EXPECT_FALSE(invalid_dv.MonthsBetween(DateValue(2001, 1, 1), &months_between));
EXPECT_FALSE(DateValue(2001, 1, 1).MonthsBetween(invalid_dv, &months_between));
// Test that if both dates are on the same day of the month, the result has no
// fractional part.
TEST_MONTHS_BW(DateValue(2016, 2, 29), DateValue(2016, 1, 29), 1);
TEST_MONTHS_BW(DateValue(2016, 2, 29), DateValue(2016, 3, 29), -1);
// Test that if both dates are on the last day of the month, the result has no
// fractional part.
TEST_MONTHS_BW(DateValue(2016, 2, 29), DateValue(2016, 1, 31), 1);
TEST_MONTHS_BW(DateValue(2016, 2, 29), DateValue(2016, 3, 31), -1);
// Otherwise, there's a fractional part.
// There are 30/31.0 months between 2016-02-29 and 2016-01-30.
TEST_MONTHS_BW_RANGE(DateValue(2016, 2, 29), DateValue(2016, 1, 30), 29/31.0, 1.0);
// There are -32/31.0 months between 2016-02-29 and 2016-03-30.
TEST_MONTHS_BW_RANGE(DateValue(2016, 2, 29), DateValue(2016, 3, 30), -33/31.0, -1.0);
// There are 28/31.0 months between 2016-02-29 and 2016-02-01.
TEST_MONTHS_BW_RANGE(DateValue(2016, 2, 29), DateValue(2016, 2, 1), 27/31.0, 29/31.0);
// There are -30/31.0 months between 2016-02-29 and 2016-03-28.
TEST_MONTHS_BW_RANGE(DateValue(2016, 2, 29), DateValue(2016, 3, 28), -31/31.0,
-29/31.0);
// Test entire range w/o fractional part.
TEST_MONTHS_BW(DateValue(1, 1, 1), DateValue(9999, 12, 1), -9998 * 12 - 11);
TEST_MONTHS_BW(DateValue(9999, 12, 31), DateValue(1, 1, 31), 9998 * 12 + 11);
// Test entire range w/ fractional part.
// There are (-9998*12 - 11 - 30/31.0) months between 0001-01-01 and 9999-12-31.
TEST_MONTHS_BW_RANGE(DateValue(1, 1, 1), DateValue(9999, 12, 31), -9999 * 12.0,
-9998 * 12 - 11 - 29/31.0);
// There are (9998*12 + 11 + 30/31.0) months between 9999-12-31 and 0001-01-01.
TEST_MONTHS_BW_RANGE(DateValue(9999, 12, 31), DateValue(1, 1, 1),
9998 * 12 + 11 + 29/31.0, 9999 * 12.0);
}
}