text: /**
   *    Copyright (C) 2018-present MongoDB, Inc.
   *
   *    This program is free software: you can redistribute it and/or modify
   *    it under the terms of the Server Side Public License, version 1,
   *    as published by MongoDB, Inc.
   *
   *    This program is distributed in the hope that it will be useful,
   *    but WITHOUT ANY WARRANTY; without even the implied warranty of
   *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   *    Server Side Public License for more details.
   *
   *    You should have received a copy of the Server Side Public License
   *    along with this program. If not, see
   *    <http://www.mongodb.com/licensing/server-side-public-license>.
   *
   *    As a special exception, the copyright holders give permission to link the
   *    code of portions of this program with the OpenSSL library under certain
   *    conditions as described in each individual source file and distribute
   *    linked combinations including the program with the OpenSSL library. You
   *    must comply with the Server Side Public License in all respects for
   *    all of the code used other than as permitted herein. If you modify file(s)
   *    with this exception, you may extend this exception to your version of the
   *    file(s), but you are not obligated to do so. If you do not wish to do so,
   *    delete this exception statement from your version. If you delete this
   *    exception statement from all source files in the program, then also delete
   *    it in the license file.
   */
  
  #include "mongo/platform/basic.h"
  
  #include <limits>
  #include <sstream>
  #include <timelib.h>
  
  #include "mongo/db/query/datetime/date_time_support.h"
  #include "mongo/unittest/unittest.h"
  
  namespace mongo {
  namespace {
  
  const TimeZoneDatabase kDefaultTimeZoneDatabase{};
  const TimeZone kDefaultTimeZone = TimeZoneDatabase::utcZone();
  
  TEST(GetTimeZone, DoesReturnKnownTimeZone) {
      // Just asserting that these do not throw exceptions.
      kDefaultTimeZoneDatabase.getTimeZone("UTC");
      kDefaultTimeZoneDatabase.getTimeZone("America/New_York");
      kDefaultTimeZoneDatabase.getTimeZone("Australia/Sydney");
  }
  
  TEST(GetTimeZone, DoesParseHourOnlyOffset) {
      auto date = Date_t::fromMillisSinceEpoch(1500371861000LL);
  
      auto zone = kDefaultTimeZoneDatabase.getTimeZone("+02");
      ASSERT_EQ(durationCount<Hours>(zone.utcOffset(date)), 2);
  
      zone = kDefaultTimeZoneDatabase.getTimeZone("-02");
      ASSERT_EQ(durationCount<Hours>(zone.utcOffset(date)), -2);
  
      zone = kDefaultTimeZoneDatabase.getTimeZone("+00");
      ASSERT_EQ(durationCount<Seconds>(zone.utcOffset(date)), 0);
  
      zone = kDefaultTimeZoneDatabase.getTimeZone("-00");
      ASSERT_EQ(durationCount<Seconds>(zone.utcOffset(date)), 0);
  }
  
  TEST(GetTimeZone, DoesParseHourMinuteOffset) {
      auto date = Date_t::fromMillisSinceEpoch(1500371861000LL);
  
      auto zone = kDefaultTimeZoneDatabase.getTimeZone("+0200");
      ASSERT_EQ(durationCount<Hours>(zone.utcOffset(date)), 2);
  
      zone = kDefaultTimeZoneDatabase.getTimeZone("-0200");
      ASSERT_EQ(durationCount<Hours>(zone.utcOffset(date)), -2);
  
      zone = kDefaultTimeZoneDatabase.getTimeZone("+0245");
      ASSERT_EQ(durationCount<Minutes>(zone.utcOffset(date)), 165);
  
      zone = kDefaultTimeZoneDatabase.getTimeZone("-0245");
      ASSERT_EQ(durationCount<Minutes>(zone.utcOffset(date)), -165);
  }
  
  TEST(GetTimeZone, DoesParseHourMinuteOffsetWithColon) {
      auto date = Date_t::fromMillisSinceEpoch(1500371861000LL);
  
      auto zone = kDefaultTimeZoneDatabase.getTimeZone("+12:00");
      ASSERT_EQ(durationCount<Hours>(zone.utcOffset(date)), 12);
  
      zone = kDefaultTimeZoneDatabase.getTimeZone("-11:00");
      ASSERT_EQ(durationCount<Hours>(zone.utcOffset(date)), -11);
  
      zone = kDefaultTimeZoneDatabase.getTimeZone("+09:27");
      ASSERT_EQ(durationCount<Minutes>(zone.utcOffset(date)), 567);
  
      zone = kDefaultTimeZoneDatabase.getTimeZone("-00:37");
      ASSERT_EQ(durationCount<Minutes>(zone.utcOffset(date)), -37);
  }
  
  TEST(GetTimeZone, DoesNotReturnUnknownTimeZone) {
      ASSERT_THROWS_CODE(kDefaultTimeZoneDatabase.getTimeZone("The moon"), AssertionException, 40485);
      ASSERT_THROWS_CODE(kDefaultTimeZoneDatabase.getTimeZone("xyz"), AssertionException, 40485);
      ASSERT_THROWS_CODE(kDefaultTimeZoneDatabase.getTimeZone("Jupiter"), AssertionException, 40485);
  }
  
  TEST(GetTimeZone, ThrowsUserExceptionIfGivenUnparsableUtcOffset) {
      ASSERT_THROWS_CODE(kDefaultTimeZoneDatabase.getTimeZone("123"), AssertionException, 40485);
      ASSERT_THROWS_CODE(kDefaultTimeZoneDatabase.getTimeZone("1234"), AssertionException, 40485);
      ASSERT_THROWS_CODE(kDefaultTimeZoneDatabase.getTimeZone("12345"), AssertionException, 40485);
      ASSERT_THROWS_CODE(kDefaultTimeZoneDatabase.getTimeZone("-123"), AssertionException, 40485);
      ASSERT_THROWS_CODE(kDefaultTimeZoneDatabase.getTimeZone("-12*34"), AssertionException, 40485);
      ASSERT_THROWS_CODE(kDefaultTimeZoneDatabase.getTimeZone("-1:23"), AssertionException, 40485);
      ASSERT_THROWS_CODE(kDefaultTimeZoneDatabase.getTimeZone("-12:3"), AssertionException, 40485);
      ASSERT_THROWS_CODE(kDefaultTimeZoneDatabase.getTimeZone("+123"), AssertionException, 40485);
      ASSERT_THROWS_CODE(kDefaultTimeZoneDatabase.getTimeZone("+12*34"), AssertionException, 40485);
      ASSERT_THROWS_CODE(kDefaultTimeZoneDatabase.getTimeZone("+1:23"), AssertionException, 40485);
      ASSERT_THROWS_CODE(kDefaultTimeZoneDatabase.getTimeZone("+12:3"), AssertionException, 40485);
      ASSERT_THROWS_CODE(kDefaultTimeZoneDatabase.getTimeZone("+0x4"), AssertionException, 40485);
      ASSERT_THROWS_CODE(kDefaultTimeZoneDatabase.getTimeZone("-0xa0"), AssertionException, 40485);
  }
  
  TEST(UTCTimeBeforeEpoch, DoesExtractDateParts) {
      // Dec 30, 1969 13:42:23:211
      auto date = Date_t::fromMillisSinceEpoch(-123456789LL);
      auto dateParts = TimeZoneDatabase::utcZone().dateParts(date);
      ASSERT_EQ(dateParts.year, 1969);
      ASSERT_EQ(dateParts.month, 12);
      ASSERT_EQ(dateParts.dayOfMonth, 30);
      ASSERT_EQ(dateParts.hour, 13);
      ASSERT_EQ(dateParts.minute, 42);
      ASSERT_EQ(dateParts.second, 23);
      ASSERT_EQ(dateParts.millisecond, 211);
  }
  
  TEST(NewYorkTimeBeforeEpoch, DoesExtractDateParts) {
      auto newYorkZone = kDefaultTimeZoneDatabase.getTimeZone("America/New_York");
  
      // 1969-12-30T13:42:23.211Z.
      auto date = Date_t::fromMillisSinceEpoch(-123456789LL);
      auto dateParts = newYorkZone.dateParts(date);
      ASSERT_EQ(dateParts.year, 1969);
      ASSERT_EQ(dateParts.month, 12);
      ASSERT_EQ(dateParts.dayOfMonth, 30);
      ASSERT_EQ(dateParts.hour, 8);
      ASSERT_EQ(dateParts.minute, 42);
      ASSERT_EQ(dateParts.second, 23);
      ASSERT_EQ(dateParts.millisecond, 211);
  }
  
  TEST(UtcOffsetBeforeEpoch, DoesExtractDateParts) {
      auto utcOffsetZone = kDefaultTimeZoneDatabase.getTimeZone("-05:00");
  
      // 1969-12-30T13:42:23.211Z.
      auto date = Date_t::fromMillisSinceEpoch(-123456789LL);
      auto dateParts = utcOffsetZone.dateParts(date);
      ASSERT_EQ(dateParts.year, 1969);
      ASSERT_EQ(dateParts.month, 12);
      ASSERT_EQ(dateParts.dayOfMonth, 30);
      ASSERT_EQ(dateParts.hour, 8);
      ASSERT_EQ(dateParts.minute, 42);
      ASSERT_EQ(dateParts.second, 23);
      ASSERT_EQ(dateParts.millisecond, 211);
  }
  
  TEST(UTCTimeBeforeEpoch, DoesComputeISOYear) {
      // Sunday, December 28, 1969.
      auto date = Date_t::fromMillisSinceEpoch(-345600000LL);
      ASSERT_EQ(TimeZoneDatabase::utcZone().isoYear(date), 1969);
      // Tue, December 30, 1969, part of the following year.
      date = Date_t::fromMillisSinceEpoch(-123456000LL);
      ASSERT_EQ(TimeZoneDatabase::utcZone().isoYear(date), 1970);
      // Saturday, January 1, 1966, part of the previous year.
      date = Date_t::fromMillisSinceEpoch(-126230400000LL);
      ASSERT_EQ(TimeZoneDatabase::utcZone().isoYear(date), 1965);
  }
  
  TEST(NewYorkTimeBeforeEpoch, DoesComputeISOYear) {
      auto newYorkZone = kDefaultTimeZoneDatabase.getTimeZone("America/New_York");
  
      // Sunday, December 28, 1969.
      auto date = Date_t::fromMillisSinceEpoch(-345600000LL);
      ASSERT_EQ(newYorkZone.isoYear(date), 1969);
  
      // 1969-12-30T13:42:23.211Z (Tuesday), part of the following year.
      date = Date_t::fromMillisSinceEpoch(-123456000LL);
      ASSERT_EQ(newYorkZone.isoYear(date), 1970);
  
      // 1966-01-01T00:00:00.000Z (Saturday), part of the previous year.
      date = Date_t::fromMillisSinceEpoch(-126230400000LL);
      ASSERT_EQ(newYorkZone.isoYear(date), 1965);
  }
  
  TEST(UTCTimeBeforeEpoch, DoesComputeDayOfWeek) {
      // Sunday, December 28, 1969.
      auto date = Date_t::fromMillisSinceEpoch(-345600000LL);
      ASSERT_EQ(TimeZoneDatabase::utcZone().dayOfWeek(date), 1);
      // Tuesday, December 30, 1969.
      date = Date_t::fromMillisSinceEpoch(-123456000LL);
      ASSERT_EQ(TimeZoneDatabase::utcZone().dayOfWeek(date), 3);
      // Saturday, January 1, 1966.
      date = Date_t::fromMillisSinceEpoch(-126230400000LL);
      ASSERT_EQ(TimeZoneDatabase::utcZone().dayOfWeek(date), 7);
  }
  
  TEST(NewYorkTimeBeforeEpoch, DoesComputeDayOfWeek) {
      auto newYorkZone = kDefaultTimeZoneDatabase.getTimeZone("America/New_York");
  
      // 1969-12-28T00:00:00.000Z (Sunday).
      auto date = Date_t::fromMillisSinceEpoch(-345600000LL);
      // Part of the previous day (Saturday) in New York.
      ASSERT_EQ(newYorkZone.dayOfWeek(date), 7);
  
      // 1969-12-30T13:42:23.211Z (Tuesday).
      date = Date_t::fromMillisSinceEpoch(-123456000LL);
      ASSERT_EQ(newYorkZone.dayOfWeek(date), 3);
  }
  
  TEST(UTCTimeBeforeEpoch, DoesComputeISODayOfWeek) {
      // Sunday, December 28, 1969.
      auto date = Date_t::fromMillisSinceEpoch(-345600000LL);
      ASSERT_EQ(TimeZoneDatabase::utcZone().isoDayOfWeek(date), 7);
      // Tue, December 30, 1969.
      date = Date_t::fromMillisSinceEpoch(-123456000LL);
      ASSERT_EQ(TimeZoneDatabase::utcZone().isoDayOfWeek(date), 2);
      // Saturday, January 1, 1966.
      date = Date_t::fromMillisSinceEpoch(-126230400000LL);
      ASSERT_EQ(TimeZoneDatabase::utcZone().isoDayOfWeek(date), 6);
  }
  
  TEST(NewYorkTimeBeforeEpoch, DoesComputeISODayOfWeek) {
      auto newYorkZone = kDefaultTimeZoneDatabase.getTimeZone("America/New_York");
  
      // 1969-12-28T00:00:00.000Z (Sunday).
      auto date = Date_t::fromMillisSinceEpoch(-345600000LL);
      // Part of the previous day (Saturday) in New York.
      ASSERT_EQ(newYorkZone.isoDayOfWeek(date), 6);
  
      // 1969-12-30T13:42:23.211Z (Tuesday).
      date = Date_t::fromMillisSinceEpoch(-123456000LL);
      ASSERT_EQ(newYorkZone.isoDayOfWeek(date), 2);
  }
  
  TEST(UTCTimeBeforeEpoch, DoesComputeDayOfYear) {
      // December 30, 1969.
      auto date = Date_t::fromMillisSinceEpoch(-123456000LL);
      ASSERT_EQ(TimeZoneDatabase::utcZone().dayOfYear(date), 364);
      // January 1, 1966.
      date = Date_t::fromMillisSinceEpoch(-126230400000LL);
      ASSERT_EQ(TimeZoneDatabase::utcZone().dayOfYear(date), 1);
  
      // Feb 28, 1960 (leap year).
      date = Date_t::fromMillisSinceEpoch(-310608000000LL);
      ASSERT_EQ(TimeZoneDatabase::utcZone().dayOfYear(date), 59);
      // Feb 29, 1960 (leap year).
      date = Date_t::fromMillisSinceEpoch(-310521600000LL);
      ASSERT_EQ(TimeZoneDatabase::utcZone().dayOfYear(date), 60);
      // Mar 1, 1960 (leap year).
      date = Date_t::fromMillisSinceEpoch(-310435200000LL);
      ASSERT_EQ(TimeZoneDatabase::utcZone().dayOfYear(date), 61);
      // December 31, 1960 (leap year).
      date = Date_t::fromMillisSinceEpoch(-284083200000LL);
      ASSERT_EQ(TimeZoneDatabase::utcZone().dayOfYear(date), 366);
  
      // Feb 28, 1900 (not leap year).
      date = Date_t::fromMillisSinceEpoch(-2203977600000LL);
      ASSERT_EQ(TimeZoneDatabase::utcZone().dayOfYear(date), 59);
      // Mar 1, 1900 (not leap year).
      date = Date_t::fromMillisSinceEpoch(-2203891200000LL);
      ASSERT_EQ(TimeZoneDatabase::utcZone().dayOfYear(date), 60);
      // December 31, 1900 (not leap year).
      date = Date_t::fromMillisSinceEpoch(-2177539200000LL);
      ASSERT_EQ(TimeZoneDatabase::utcZone().dayOfYear(date), 365);
  }
  
  TEST(NewYorkTimeBeforeEpoch, DoesComputeDayOfYear) {
      auto newYorkZone = kDefaultTimeZoneDatabase.getTimeZone("America/New_York");
  
      // 1969-12-28T13:42:24.000Z (Sunday).
      auto date = Date_t::fromMillisSinceEpoch(-123456000LL);
      ASSERT_EQ(newYorkZone.dayOfYear(date), 364);
  
      // 1966-01-01T00:00:00.000Z (Saturday).
      date = Date_t::fromMillisSinceEpoch(-126230400000LL);
      ASSERT_EQ(newYorkZone.dayOfYear(date), 365);
  
      // 1960-02-28T00:00:00.000Z (leap year).
      date = Date_t::fromMillisSinceEpoch(-310608000000LL);
      ASSERT_EQ(newYorkZone.dayOfYear(date), 58);
      // 1960-02-29T00:00:00.000Z.
      date = Date_t::fromMillisSinceEpoch(-310521600000LL);
      ASSERT_EQ(newYorkZone.dayOfYear(date), 59);
      // 1960-01-01T00:00:00.000Z.
      date = Date_t::fromMillisSinceEpoch(-310435200000LL);
      ASSERT_EQ(newYorkZone.dayOfYear(date), 60);
      // 1960-12-31T00:00:00.000Z.
      date = Date_t::fromMillisSinceEpoch(-284083200000LL);
      ASSERT_EQ(newYorkZone.dayOfYear(date), 365);
  
      // 1900-02-28T00:00:00.000Z (not leap year).
      date = Date_t::fromMillisSinceEpoch(-2203977600000LL);
      ASSERT_EQ(newYorkZone.dayOfYear(date), 58);
      // 1900-03-01T00:00:00.000Z.
      date = Date_t::fromMillisSinceEpoch(-2203891200000LL);
      ASSERT_EQ(newYorkZone.dayOfYear(date), 59);
      // 1900-12-31T00:00:00.000Z.
      date = Date_t::fromMillisSinceEpoch(-2177539200000LL);
      ASSERT_EQ(newYorkZone.dayOfYear(date), 364);
  }
  
  TEST(UTCTimeBeforeEpoch, DoesComputeWeek) {
      // Sunday, December 28, 1969.
      auto date = Date_t::fromMillisSinceEpoch(-345600000LL);
      ASSERT_EQ(TimeZoneDatabase::utcZone().week(date), 52);
      // Saturday, January 1, 1966.
      date = Date_t::fromMillisSinceEpoch(-126230400000LL);
      ASSERT_EQ(TimeZoneDatabase::utcZone().week(date), 0);
  }
  
  TEST(NewYorkTimeBeforeEpoch, DoesComputeWeek) {
      auto newYorkZone = kDefaultTimeZoneDatabase.getTimeZone("America/New_York");
  
      // 1969-12-28T00:00:00.000Z (Sunday).
      auto date = Date_t::fromMillisSinceEpoch(-345600000LL);
      ASSERT_EQ(newYorkZone.week(date), 51);
  
      // 1966-01-01T00:00:00.000Z (Saturday).
      date = Date_t::fromMillisSinceEpoch(-126230400000LL);
      ASSERT_EQ(newYorkZone.week(date), 52);
  }
  
  TEST(UTCTimeBeforeEpoch, DoesComputeUtcOffset) {
      // Sunday, December 28, 1969.
      auto date = Date_t::fromMillisSinceEpoch(-345600000LL);
      ASSERT_EQ(durationCount<Seconds>(TimeZoneDatabase::utcZone().utcOffset(date)), 0);
  }
  
  TEST(NewYorkTimeBeforeEpoch, DoesComputeUtcOffset) {
      auto newYorkZone = kDefaultTimeZoneDatabase.getTimeZone("America/New_York");
  
      // 1969-06-29T00:00:00.000Z (Sunday).
      auto date = Date_t::fromMillisSinceEpoch(-16070400000LL);
      ASSERT_EQ(durationCount<Hours>(newYorkZone.utcOffset(date)), -4);
  
      // 1966-01-01T00:00:00.000Z (Saturday).
      date = Date_t::fromMillisSinceEpoch(-126230400000LL);
      ASSERT_EQ(durationCount<Hours>(newYorkZone.utcOffset(date)), -5);
  }
  
  TEST(UTCTimeBeforeEpoch, DoesComputeISOWeek) {
      // Sunday, December 28, 1969.
      auto date = Date_t::fromMillisSinceEpoch(-345600000LL);
      ASSERT_EQ(TimeZoneDatabase::utcZone().isoWeek(date), 52);
      // Tuesday, December 30, 1969.
      date = Date_t::fromMillisSinceEpoch(-123456000LL);
      ASSERT_EQ(TimeZoneDatabase::utcZone().isoWeek(date), 1);
      // Saturday, January 1, 1966, part of previous year.
      date = Date_t::fromMillisSinceEpoch(-126230400000LL);
      ASSERT_EQ(TimeZoneDatabase::utcZone().isoWeek(date), 52);
      // Tuesday, December 29, 1959.
      date = Date_t::fromMillisSinceEpoch(-315878400000LL);
      ASSERT_EQ(TimeZoneDatabase::utcZone().isoWeek(date), 53);
      // Saturday, January 2, 1960, part of previous ISO year.
      date = Date_t::fromMillisSinceEpoch(-315532800000LL);
      ASSERT_EQ(TimeZoneDatabase::utcZone().isoWeek(date), 53);
  }
  
  TEST(NewYorkTimeBeforeEpoch, DoesComputeISOWeek) {
      auto newYorkZone = kDefaultTimeZoneDatabase.getTimeZone("America/New_York");
  
      // 1969-12-28T00:00:00.000Z (Sunday).
      auto date = Date_t::fromMillisSinceEpoch(-345600000LL);
      ASSERT_EQ(newYorkZone.isoWeek(date), 52);
  
      // 1969-12-30T00:00:00.000Z (Tuesday).
      date = Date_t::fromMillisSinceEpoch(-123456000LL);
      ASSERT_EQ(newYorkZone.isoWeek(date), 1);
  
      // 1966-01-01T00:00:00.000Z (Saturday), part of previous year.
      date = Date_t::fromMillisSinceEpoch(-126230400000LL);
      ASSERT_EQ(newYorkZone.isoWeek(date), 52);
      // 1959-12-29T00:00:00.000Z (Tuesday).
      date = Date_t::fromMillisSinceEpoch(-315878400000LL);
      ASSERT_EQ(newYorkZone.isoWeek(date), 53);
      // 1960-01-02T00:00:00.000Z (Saturday), part of previous year.
      date = Date_t::fromMillisSinceEpoch(-315532800000LL);
      ASSERT_EQ(newYorkZone.isoWeek(date), 53);
  }
  
  TEST(UTCTimeBeforeEpoch, DoesFormatDate) {
      // Tuesday, Dec 30, 1969 13:42:23:211
      auto date = Date_t::fromMillisSinceEpoch(-123456789LL);
      auto result = TimeZoneDatabase::utcZone().formatDate(
          "%Y/%m/%d %H:%M:%S:%L, dayOfYear: %j, "
          "dayOfWeek: %w, week: %U, isoYear: %G, "
          "isoWeek: %V, isoDayOfWeek: %u, percent: %%",
          date);
      ASSERT_OK(result);
      ASSERT_EQ(
          "1969/12/30 13:42:23:211, dayOfYear: 364, dayOfWeek: 3, week: 52, isoYear: 1970, "
          "isoWeek: 01, isoDayOfWeek: 2, percent: %",
          result.getValue());
  }
  
  TEST(NewYorkTimeBeforeEpoch, DoesFormatDate) {
      auto newYorkZone = kDefaultTimeZoneDatabase.getTimeZone("America/New_York");
  
      // Tuesday, Dec 30, 1969 13:42:23:211
      auto date = Date_t::fromMillisSinceEpoch(-123456789LL);
      auto result = newYorkZone.formatDate(
          "%Y/%m/%d %H:%M:%S:%L, dayOfYear: %j, "
          "dayOfWeek: %w, week: %U, isoYear: %G, "
          "isoWeek: %V, isoDayOfWeek: %u, percent: %%",
          date);
      ASSERT_OK(result);
      ASSERT_EQ(
          "1969/12/30 08:42:23:211, dayOfYear: 364, dayOfWeek: 3, week: 52, isoYear: 1970, "
          "isoWeek: 01, isoDayOfWeek: 2, percent: %",
          result.getValue());
  }
  
  TEST(UTCTimeBeforeEpoch, DoesOutputFormatDate) {
      // Tuesday, Dec 30, 1969 13:42:23:211
      auto date = Date_t::fromMillisSinceEpoch(-123456789LL);
      std::ostringstream os;
      ASSERT_OK(TimeZoneDatabase::utcZone().outputDateWithFormat(
          os,
          "%Y/%m/%d %H:%M:%S:%L, dayOfYear: %j, "
          "dayOfWeek: %w, week: %U, isoYear: %G, "
          "isoWeek: %V, isoDayOfWeek: %u, percent: %%",
          date));
      ASSERT_EQ(os.str(),
                "1969/12/30 13:42:23:211, dayOfYear: 364, dayOfWeek: 3, week: 52, isoYear: 1970, "
                "isoWeek: 01, isoDayOfWeek: 2, percent: %");
  }
  
  TEST(NewYorkTimeBeforeEpoch, DoesOutputFormatDate) {
      auto newYorkZone = kDefaultTimeZoneDatabase.getTimeZone("America/New_York");
  
      // Tuesday, Dec 30, 1969 13:42:23:211
      auto date = Date_t::fromMillisSinceEpoch(-123456789LL);
      std::ostringstream os;
      ASSERT_OK(newYorkZone.outputDateWithFormat(os,
                                                 "%Y/%m/%d %H:%M:%S:%L, dayOfYear: %j, "
                                                 "dayOfWeek: %w, week: %U, isoYear: %G, "
                                                 "isoWeek: %V, isoDayOfWeek: %u, percent: %%",
                                                 date));
      ASSERT_EQ(os.str(),
                "1969/12/30 08:42:23:211, dayOfYear: 364, dayOfWeek: 3, week: 52, isoYear: 1970, "
                "isoWeek: 01, isoDayOfWeek: 2, percent: %");
  }
  
  TEST(UTCTimeAtEpoch, DoesExtractDateParts) {
      // Jan 1, 1970 00:00:00:000
      auto date = Date_t::fromMillisSinceEpoch(0);
      auto dateParts = TimeZoneDatabase::utcZone().dateParts(date);
      ASSERT_EQ(dateParts.year, 1970);
      ASSERT_EQ(dateParts.month, 1);
      ASSERT_EQ(dateParts.dayOfMonth, 1);
      ASSERT_EQ(dateParts.hour, 0);
      ASSERT_EQ(dateParts.minute, 0);
      ASSERT_EQ(dateParts.second, 0);
      ASSERT_EQ(dateParts.millisecond, 0);
  }
  
  TEST(NewYorkTimeAtEpoch, DoesExtractDateParts) {
      auto newYorkZone = kDefaultTimeZoneDatabase.getTimeZone("America/New_York");
  
      // Jan 1, 1970 00:00:00:000
      auto date = Date_t::fromMillisSinceEpoch(0);
      auto dateParts = newYorkZone.dateParts(date);
      ASSERT_EQ(dateParts.year, 1969);
      ASSERT_EQ(dateParts.month, 12);
      ASSERT_EQ(dateParts.dayOfMonth, 31);
      ASSERT_EQ(dateParts.hour, 19);
      ASSERT_EQ(dateParts.minute, 0);
      ASSERT_EQ(dateParts.second, 0);
      ASSERT_EQ(dateParts.millisecond, 0);
  }
  
  TEST(UTCTimeAtEpoch, DoesComputeISOYear) {
      // Thursday, January 1, 1970.
      auto date = Date_t::fromMillisSinceEpoch(0);
      ASSERT_EQ(TimeZoneDatabase::utcZone().isoYear(date), 1970);
  }
  
  TEST(NewYorkTimeAtEpoch, DoesComputeISOYear) {
      auto newYorkZone = kDefaultTimeZoneDatabase.getTimeZone("America/New_York");
  
      // 1970-1-1T00:00:00.000Z (Thursday)
      auto date = Date_t::fromMillisSinceEpoch(0);
      ASSERT_EQ(newYorkZone.isoYear(date), 1970);  // The Wednesday is still considered part of 1970.
  }
  
  TEST(UTCTimeAtEpoch, DoesComputeDayOfWeek) {
      // Thursday, January 1, 1970.
      auto date = Date_t::fromMillisSinceEpoch(0);
      ASSERT_EQ(TimeZoneDatabase::utcZone().dayOfWeek(date), 5);
  }
  
  TEST(NewYorkTimeAtEpoch, DoesComputeDayOfWeek) {
      auto newYorkZone = kDefaultTimeZoneDatabase.getTimeZone("America/New_York");
  
      // 1970-1-1T00:00:00.000Z (Thursday)
      auto date = Date_t::fromMillisSinceEpoch(0);
      ASSERT_EQ(newYorkZone.dayOfWeek(date), 4);  // The Wednesday before.
  }
  
  TEST(UTCTimeAtEpoch, DoesComputeISODayOfWeek) {
      // Thursday, January 1, 1970.
      auto date = Date_t::fromMillisSinceEpoch(0);
      ASSERT_EQ(TimeZoneDatabase::utcZone().isoDayOfWeek(date), 4);
  }
  
  TEST(NewYorkTimeAtEpoch, DoesComputeISODayOfWeek) {
      // 1970-1-1T00:00:00.000Z (Thursday)
      auto date = Date_t::fromMillisSinceEpoch(0);
      auto newYorkZone = kDefaultTimeZoneDatabase.getTimeZone("America/New_York");
      ASSERT_EQ(newYorkZone.isoDayOfWeek(date), 3);  // The Wednesday before.
  }
  
  TEST(UTCTimeAtEpoch, DoesComputeDayOfYear) {
      // Thursday, January 1, 1970.
      auto date = Date_t::fromMillisSinceEpoch(0);
      ASSERT_EQ(TimeZoneDatabase::utcZone().dayOfYear(date), 1);
  }
  
  TEST(NewYorkTimeAtEpoch, DoesComputeDayOfYear) {
      auto newYorkZone = kDefaultTimeZoneDatabase.getTimeZone("America/New_York");
  
      // 1970-1-1T00:00:00.000Z
      auto date = Date_t::fromMillisSinceEpoch(0);
      ASSERT_EQ(newYorkZone.dayOfYear(date), 365);
  }
  
  TEST(UTCTimeAtEpoch, DoesComputeWeek) {
      // Thursday, January 1, 1970.
      auto date = Date_t::fromMillisSinceEpoch(0);
      ASSERT_EQ(TimeZoneDatabase::utcZone().week(date), 0);
  }
  
  TEST(NewYorkTimeAtEpoch, DoesComputeWeek) {
      auto newYorkZone = kDefaultTimeZoneDatabase.getTimeZone("America/New_York");
  
      // 1970-1-1T00:00:00.000Z
      auto date = Date_t::fromMillisSinceEpoch(0);
      ASSERT_EQ(newYorkZone.week(date), 52);
  }
  
  TEST(UTCTimeAtEpoch, DoesComputeUtcOffset) {
      // Thursday, January 1, 1970.
      auto date = Date_t::fromMillisSinceEpoch(0);
      ASSERT_EQ(durationCount<Seconds>(TimeZoneDatabase::utcZone().utcOffset(date)), 0);
  }
  
  TEST(NewYorkTimeAtEpoch, DoesComputeUtcOffset) {
      auto newYorkZone = kDefaultTimeZoneDatabase.getTimeZone("America/New_York");
  
      // 1970-1-1T00:00:00.000Z
      auto date = Date_t::fromMillisSinceEpoch(0);
      ASSERT_EQ(durationCount<Hours>(newYorkZone.utcOffset(date)), -5);
  }
  
  TEST(UTCTimeAtEpoch, DoesComputeISOWeek) {
      // Thursday, January 1, 1970.
      auto date = Date_t::fromMillisSinceEpoch(0);
      ASSERT_EQ(TimeZoneDatabase::utcZone().isoWeek(date), 1);
  }
  
  TEST(NewYorkTimeAtEpoch, DoesComputeISOWeek) {
      auto newYorkZone = kDefaultTimeZoneDatabase.getTimeZone("America/New_York");
  
      // Thu, Jan 1, 1970 00:00:00:000Z
      auto date = Date_t::fromMillisSinceEpoch(0);
      // This is Wednesday in New York, but that is still part of the first week.
      ASSERT_EQ(newYorkZone.isoWeek(date), 1);
  }
  
  TEST(UTCTimeAtEpoch, DoesFormatDate) {
      // Thu, Jan 1, 1970 00:00:00:000
      auto date = Date_t::fromMillisSinceEpoch(0);
      auto result = TimeZoneDatabase::utcZone().formatDate(
          "%Y/%m/%d %H:%M:%S:%L, dayOfYear: %j, "
          "dayOfWeek: %w, week: %U, isoYear: %G, "
          "isoWeek: %V, isoDayOfWeek: %u, percent: %%",
          date);
      ASSERT_OK(result);
      ASSERT_EQ(
          "1970/01/01 00:00:00:000, dayOfYear: 001, dayOfWeek: 5, week: 00, isoYear: 1970, "
          "isoWeek: 01, isoDayOfWeek: 4, percent: %",
          result.getValue());
  }
  
  TEST(NewYorkTimeAtEpoch, DoesFormatDate) {
      auto newYorkZone = kDefaultTimeZoneDatabase.getTimeZone("America/New_York");
  
      // Thu, Jan 1, 1970 00:00:00:000Z
      auto date = Date_t::fromMillisSinceEpoch(0);
      auto result = newYorkZone.formatDate(
          "%Y/%m/%d %H:%M:%S:%L, dayOfYear: %j, "
          "dayOfWeek: %w, week: %U, isoYear: %G, "
          "isoWeek: %V, isoDayOfWeek: %u, percent: %%",
          date);
      ASSERT_OK(result);
      ASSERT_EQ(
          "1969/12/31 19:00:00:000, dayOfYear: 365, dayOfWeek: 4, week: 52, isoYear: 1970, "
          "isoWeek: 01, isoDayOfWeek: 3, percent: %",
          result.getValue());
  }
  
  TEST(UTCTimeAtEpoch, DoesOutputFormatDate) {
      auto date = Date_t::fromMillisSinceEpoch(0);
      std::ostringstream os;
      ASSERT_OK(TimeZoneDatabase::utcZone().outputDateWithFormat(
          os,
          "%Y/%m/%d %H:%M:%S:%L, dayOfYear: %j, "
          "dayOfWeek: %w, week: %U, isoYear: %G, "
          "isoWeek: %V, isoDayOfWeek: %u, percent: %%",
          date));
      ASSERT_EQ(os.str(),
                "1970/01/01 00:00:00:000, dayOfYear: 001, dayOfWeek: 5, week: 00, isoYear: 1970, "
                "isoWeek: 01, isoDayOfWeek: 4, percent: %");
  }
  
  TEST(NewYorkTimeAtEpoch, DoesOutputFormatDate) {
      auto newYorkZone = kDefaultTimeZoneDatabase.getTimeZone("America/New_York");
      auto date = Date_t::fromMillisSinceEpoch(0);
      std::ostringstream os;
      ASSERT_OK(newYorkZone.outputDateWithFormat(os,
                                                 "%Y/%m/%d %H:%M:%S:%L, dayOfYear: %j, "
                                                 "dayOfWeek: %w, week: %U, isoYear: %G, "
                                                 "isoWeek: %V, isoDayOfWeek: %u, percent: %%",
                                                 date));
      ASSERT_EQ(os.str(),
                "1969/12/31 19:00:00:000, dayOfYear: 365, dayOfWeek: 4, week: 52, isoYear: 1970, "
                "isoWeek: 01, isoDayOfWeek: 3, percent: %");
  }
  
  TEST(UTCTimeAfterEpoch, DoesExtractDateParts) {
      // Jun 6, 2017 19:38:43:123.
      auto date = Date_t::fromMillisSinceEpoch(1496777923123LL);
      auto dateParts = TimeZoneDatabase::utcZone().dateParts(date);
      ASSERT_EQ(dateParts.year, 2017);
      ASSERT_EQ(dateParts.month, 6);
      ASSERT_EQ(dateParts.dayOfMonth, 6);
      ASSERT_EQ(dateParts.hour, 19);
      ASSERT_EQ(dateParts.minute, 38);
      ASSERT_EQ(dateParts.second, 43);
      ASSERT_EQ(dateParts.millisecond, 123);
  }
  
  TEST(NewYorkTimeAfterEpoch, DoesExtractDateParts) {
      auto newYorkZone = kDefaultTimeZoneDatabase.getTimeZone("America/New_York");
  
      // Jun 6, 2017 19:38:43:123Z.
      auto date = Date_t::fromMillisSinceEpoch(1496777923123LL);
      auto dateParts = newYorkZone.dateParts(date);
      ASSERT_EQ(dateParts.year, 2017);
      ASSERT_EQ(dateParts.month, 6);
      ASSERT_EQ(dateParts.dayOfMonth, 6);
      ASSERT_EQ(dateParts.hour, 15);
      ASSERT_EQ(dateParts.minute, 38);
      ASSERT_EQ(dateParts.second, 43);
      ASSERT_EQ(dateParts.millisecond, 123);
  }
  
  TEST(UTCTimeAfterEpoch, DoesComputeISOYear) {
      // Tuesday, June 6, 2017.
      auto date = Date_t::fromMillisSinceEpoch(1496777923000LL);
      ASSERT_EQ(TimeZoneDatabase::utcZone().isoYear(date), 2017);
      // Saturday, January 1, 2005, part of the previous year.
      date = Date_t::fromMillisSinceEpoch(1104537600000LL);
      ASSERT_EQ(TimeZoneDatabase::utcZone().isoYear(date), 2004);
      // Monday, January 1, 2007.
      date = Date_t::fromMillisSinceEpoch(1167609600000LL);
      ASSERT_EQ(TimeZoneDatabase::utcZone().isoYear(date), 2007);
      // Monday, December 31, 2007, part of the next year.
      date = Date_t::fromMillisSinceEpoch(1199059200000LL);
      ASSERT_EQ(TimeZoneDatabase::utcZone().isoYear(date), 2008);
  }
  
  TEST(NewYorkTimeAfterEpoch, DoesComputeISOYear) {
      auto newYorkZone = kDefaultTimeZoneDatabase.getTimeZone("America/New_York");
  
      // 2017-06-06T19:38:43:123Z (Tuesday).
      auto date = Date_t::fromMillisSinceEpoch(1496777923000LL);
      ASSERT_EQ(newYorkZone.isoYear(date), 2017);
  
      // 2005-01-01T00:00:00.000Z (Saturday), part of 2004.
      date = Date_t::fromMillisSinceEpoch(1104537600000LL);
      ASSERT_EQ(newYorkZone.isoYear(date), 2004);
  
      // 2007-01-01T00:00:00.000Z (Monday), part of 2007.
      date = Date_t::fromMillisSinceEpoch(1167609600000LL);
      // ISO weeks are Mon-Sun, so this is part of the previous week in New York, so part of the
      // previous year, 2006.
      ASSERT_EQ(newYorkZone.isoYear(date), 2006);
  
      // 2007-12-31T00:00:00.000Z (Monday), part of 2007.
      date = Date_t::fromMillisSinceEpoch(1199059200000LL);
      // ISO weeks are Mon-Sun, so this is part of the previous week in New York, so part of the
      // previous year, 2007.
      ASSERT_EQ(newYorkZone.isoYear(date), 2007);
  }
  
  TEST(UTCTimeAfterEpoch, DoesComputeDayOfWeek) {
      // Tuesday, June 6, 2017.
      auto date = Date_t::fromMillisSinceEpoch(1496777923000LL);
      ASSERT_EQ(TimeZoneDatabase::utcZone().dayOfWeek(date), 3);
      // Saturday, January 1, 2005.
      date = Date_t::fromMillisSinceEpoch(1104537600000LL);
      ASSERT_EQ(TimeZoneDatabase::utcZone().dayOfWeek(date), 7);
      // Monday, January 1, 2007.
      date = Date_t::fromMillisSinceEpoch(1167609600000LL);
      ASSERT_EQ(TimeZoneDatabase::utcZone().dayOfWeek(date), 2);
  }
  
  TEST(NewYorkTimeAfterEpoch, DoesComputeDayOfWeek) {
      auto newYorkZone = kDefaultTimeZoneDatabase.getTimeZone("America/New_York");
  
      // 2017-06-06T19:38:43.123Z (Tuesday).
      auto date = Date_t::fromMillisSinceEpoch(1496777923000LL);
      ASSERT_EQ(newYorkZone.dayOfWeek(date), 3);
  
      // 2005-01-01T00:00:00.000Z (Saturday).
      date = Date_t::fromMillisSinceEpoch(1104537600000LL);
      ASSERT_EQ(newYorkZone.dayOfWeek(date), 6);  // Part of the previous day in New York.
  }
  
  TEST(UTCTimeAfterEpoch, DoesComputeISODayOfWeek) {
      // Tuesday, June 6, 2017.
      auto date = Date_t::fromMillisSinceEpoch(1496777923000LL);
      ASSERT_EQ(TimeZoneDatabase::utcZone().isoDayOfWeek(date), 2);
      // Saturday, January 1, 2005.
      date = Date_t::fromMillisSinceEpoch(1104537600000LL);
      ASSERT_EQ(TimeZoneDatabase::utcZone().isoDayOfWeek(date), 6);
      // Monday, January 1, 2007.
      date = Date_t::fromMillisSinceEpoch(1167609600000LL);
      ASSERT_EQ(TimeZoneDatabase::utcZone().isoDayOfWeek(date), 1);
  }
  
  TEST(NewYorkTimeAfterEpoch, DoesComputeISODayOfWeek) {
      auto newYorkZone = kDefaultTimeZoneDatabase.getTimeZone("America/New_York");
  
      // 2017-06-06T19:38:43.123Z (Tuesday).
      auto date = Date_t::fromMillisSinceEpoch(1496777923000LL);
      ASSERT_EQ(newYorkZone.isoDayOfWeek(date), 2);
  
      // 2005-01-01T00:00:00.000Z (Saturday).
      date = Date_t::fromMillisSinceEpoch(1104537600000LL);
      ASSERT_EQ(newYorkZone.isoDayOfWeek(date), 5);  // Part of the previous day in New York.
  }
  
  TEST(UTCTimeAfterEpoch, DoesComputeDayOfYear) {
      // June 6, 2017.
      auto date = Date_t::fromMillisSinceEpoch(1496777923000LL);
      ASSERT_EQ(TimeZoneDatabase::utcZone().dayOfYear(date), 157);
      // January 1, 2005.
      date = Date_t::fromMillisSinceEpoch(1104537600000LL);
      ASSERT_EQ(TimeZoneDatabase::utcZone().dayOfYear(date), 1);
      // Feb 28, 2008 (leap year).
      date = Date_t::fromMillisSinceEpoch(1204156800000LL);
      ASSERT_EQ(TimeZoneDatabase::utcZone().dayOfYear(date), 59);
      // Feb 29, 2008 (leap year).
      date = Date_t::fromMillisSinceEpoch(1204243200000LL);
      ASSERT_EQ(TimeZoneDatabase::utcZone().dayOfYear(date), 60);
      // Mar 1, 2008 (leap year).
      date = Date_t::fromMillisSinceEpoch(1204329600000LL);
      ASSERT_EQ(TimeZoneDatabase::utcZone().dayOfYear(date), 61);
      // December 31, 2008 (leap year).
      date = Date_t::fromMillisSinceEpoch(1230681600000LL);
      ASSERT_EQ(TimeZoneDatabase::utcZone().dayOfYear(date), 366);
  
      // Feb 28, 2001 (not leap year).
      date = Date_t::fromMillisSinceEpoch(983318400000LL);
      ASSERT_EQ(TimeZoneDatabase::utcZone().dayOfYear(date), 59);
      // Mar 1, 2001 (not leap year).
      date = Date_t::fromMillisSinceEpoch(983404800000LL);
      ASSERT_EQ(TimeZoneDatabase::utcZone().dayOfYear(date), 60);
      // December 31, 2001 (not leap year).
      date = Date_t::fromMillisSinceEpoch(1009756800000LL);
      ASSERT_EQ(TimeZoneDatabase::utcZone().dayOfYear(date), 365);
  }
  
  TEST(NewYorkTimeAfterEpoch, DoesComputeDayOfYear) {
      auto newYorkZone = kDefaultTimeZoneDatabase.getTimeZone("America/New_York");
  
      // 2017-06-06T19:38:43.123Z.
      auto date = Date_t::fromMillisSinceEpoch(1496777923000LL);
      ASSERT_EQ(newYorkZone.dayOfYear(date), 157);
  
      // 2005-01-01T00:00:00.000Z, part of 2004 in New York, which was a leap year.
      date = Date_t::fromMillisSinceEpoch(1104537600000LL);
      ASSERT_EQ(newYorkZone.dayOfYear(date), 366);
  
      // 2008-02-28T00:00:00.000Z (leap year).
      date = Date_t::fromMillisSinceEpoch(1204156800000LL);
      ASSERT_EQ(newYorkZone.dayOfYear(date), 58);
      // 2008-02-29T00:00:00.000Z.
      date = Date_t::fromMillisSinceEpoch(1204243200000LL);
      ASSERT_EQ(newYorkZone.dayOfYear(date), 59);
      // 2008-03-01T00:00:00.000Z.
      date = Date_t::fromMillisSinceEpoch(1204329600000LL);
      ASSERT_EQ(newYorkZone.dayOfYear(date), 60);
      // 2008-12-31T00:00:00.000Z.
      date = Date_t::fromMillisSinceEpoch(1230681600000LL);
      ASSERT_EQ(newYorkZone.dayOfYear(date), 365);
      // 2009-01-01T00:00:00.000Z, part of the previous year in New York.
      date = Date_t::fromMillisSinceEpoch(1230768000000LL);
      ASSERT_EQ(newYorkZone.dayOfYear(date), 366);
  
      // 2001-02-28T00:00:00.000Z (not leap year).
      date = Date_t::fromMillisSinceEpoch(983318400000LL);
      ASSERT_EQ(newYorkZone.dayOfYear(date), 58);
      // 2001-03-01T00:00:00.000Z.
      date = Date_t::fromMillisSinceEpoch(983404800000LL);
      ASSERT_EQ(newYorkZone.dayOfYear(date), 59);
      // 2001-12-31T00:00:00.000Z.
      date = Date_t::fromMillisSinceEpoch(1009756800000LL);
      ASSERT_EQ(newYorkZone.dayOfYear(date), 364);
      // 2002-01-01T00:00:00.000Z, part of the previous year in New York.
      date = Date_t::fromMillisSinceEpoch(1009843200000LL);
      ASSERT_EQ(newYorkZone.dayOfYear(date), 365);
  }
  
  TEST(UTCTimeAfterEpoch, DoesComputeWeek) {
      // Tuesday, June 6, 2017.
      auto date = Date_t::fromMillisSinceEpoch(1496777923000LL);
      ASSERT_EQ(TimeZoneDatabase::utcZone().week(date), 23);
      // Saturday, January 1, 2005, before first Sunday.
      date = Date_t::fromMillisSinceEpoch(1104537600000LL);
      ASSERT_EQ(TimeZoneDatabase::utcZone().week(date), 0);
      // Monday, January 1, 2007, before first Sunday.
      date = Date_t::fromMillisSinceEpoch(1167609600000LL);
      ASSERT_EQ(TimeZoneDatabase::utcZone().week(date), 0);
  }
  
  TEST(NewYorkTimeAfterEpoch, DoesComputeWeek) {
      auto newYorkZone = kDefaultTimeZoneDatabase.getTimeZone("America/New_York");
  
      // Tuesday, June 6, 2017.
      auto date = Date_t::fromMillisSinceEpoch(1496777923000LL);
      ASSERT_EQ(newYorkZone.week(date), 23);
  
      // 2005-01-01T00:00:00.00Z (Saturday).
      date = Date_t::fromMillisSinceEpoch(1104537600000LL);
      ASSERT_EQ(newYorkZone.week(date), 52);
  
      // 2007-01-01T00:00:00.00Z (Monday), the last Sunday of 2006 in New York.
      date = Date_t::fromMillisSinceEpoch(1167609600000LL);
      ASSERT_EQ(newYorkZone.week(date), 53);
  }
  
  TEST(UTCTimeAfterEpoch, DoesComputeUtcOffset) {
      // Tuesday, June 6, 2017.
      auto date = Date_t::fromMillisSinceEpoch(1496777923000LL);
      ASSERT_EQ(durationCount<Seconds>(TimeZoneDatabase::utcZone().utcOffset(date)), 0);
  }
  
  TEST(NewYorkTimeAfterEpoch, DoesComputeUtcOffset) {
      auto newYorkZone = kDefaultTimeZoneDatabase.getTimeZone("America/New_York");
  
      // Tuesday, June 6, 2017.
      auto date = Date_t::fromMillisSinceEpoch(1496777923000LL);
      ASSERT_EQ(durationCount<Hours>(newYorkZone.utcOffset(date)), -4);
  
      // 2005-01-01T00:00:00.00Z (Saturday).
      date = Date_t::fromMillisSinceEpoch(1104537600000LL);
      ASSERT_EQ(durationCount<Hours>(newYorkZone.utcOffset(date)), -5);
  }
  
  TEST(UTCTimeAfterEpoch, DoesComputeISOWeek) {
      // Tuesday, June 6, 2017.
      auto date = Date_t::fromMillisSinceEpoch(1496777923000LL);
      ASSERT_EQ(TimeZoneDatabase::utcZone().isoWeek(date), 23);
      // Saturday, January 1, 2005, considered part of 2004, which was a leap year.
      date = Date_t::fromMillisSinceEpoch(1104537600000LL);
      ASSERT_EQ(TimeZoneDatabase::utcZone().isoWeek(date), 53);
      // Monday, January 1, 2007.
      date = Date_t::fromMillisSinceEpoch(1167609600000LL);
      ASSERT_EQ(TimeZoneDatabase::utcZone().isoWeek(date), 1);
  }
  
  TEST(NewYorkTimeAfterEpoch, DoesComputeISOWeek) {
      auto newYorkZone = kDefaultTimeZoneDatabase.getTimeZone("America/New_York");
  
      // Tuesday, June 6, 2017.
      auto date = Date_t::fromMillisSinceEpoch(1496777923000LL);
      ASSERT_EQ(newYorkZone.isoWeek(date), 23);
  
      // 2005-01-01T00:00:00.000Z (Saturday), part of 2004.
      date = Date_t::fromMillisSinceEpoch(1104537600000LL);
      ASSERT_EQ(newYorkZone.isoWeek(date), 53);
  
      // 2007-01-01T00:00:00.000Z (Monday), part of 2006 in New York.
      date = Date_t::fromMillisSinceEpoch(1167609600000LL);
      ASSERT_EQ(newYorkZone.isoWeek(date), 52);
  }
  
  TEST(UTCTimeAfterEpoch, DoesFormatDate) {
      // Tue, Jun 6, 2017 19:38:43:234.
      auto date = Date_t::fromMillisSinceEpoch(1496777923234LL);
      auto result = TimeZoneDatabase::utcZone().formatDate(
          "%Y/%m/%d %H:%M:%S:%L, dayOfYear: %j, "
          "dayOfWeek: %w, week: %U, isoYear: %G, "
          "isoWeek: %V, isoDayOfWeek: %u, percent: %%",
          date);
      ASSERT_OK(result);
      ASSERT_EQ(
          "2017/06/06 19:38:43:234, dayOfYear: 157, dayOfWeek: 3, week: 23, isoYear: 2017, "
          "isoWeek: 23, isoDayOfWeek: 2, percent: %",
          result.getValue());
  }
  
  TEST(NewYorkTimeAfterEpoch, DoesFormatDate) {
      // 2017-06-06T19:38:43:234Z (Tuesday).
      auto date = Date_t::fromMillisSinceEpoch(1496777923234LL);
      auto newYorkZone = kDefaultTimeZoneDatabase.getTimeZone("America/New_York");
      auto result = newYorkZone.formatDate(
          "%Y/%m/%d %H:%M:%S:%L, dayOfYear: %j, "
          "dayOfWeek: %w, week: %U, isoYear: %G, "
          "isoWeek: %V, isoDayOfWeek: %u, percent: %%",
          date);
      ASSERT_OK(result);
      ASSERT_EQ(
          "2017/06/06 15:38:43:234, dayOfYear: 157, dayOfWeek: 3, week: 23, isoYear: 2017, "
          "isoWeek: 23, isoDayOfWeek: 2, percent: %",
          result.getValue());
  }
  
  TEST(UTCOffsetAfterEpoch, DoesFormatDate) {
      // 2017-06-06T19:38:43:234Z (Tuesday).
      auto date = Date_t::fromMillisSinceEpoch(1496777923234LL);
      auto offsetSpec = kDefaultTimeZoneDatabase.getTimeZone("+02:30");
      auto result = offsetSpec.formatDate(
          "%Y/%m/%d %H:%M:%S:%L, dayOfYear: %j, "
          "dayOfWeek: %w, week: %U, isoYear: %G, "
          "isoWeek: %V, isoDayOfWeek: %u, percent: %%",
          date);
      ASSERT_OK(result);
      ASSERT_EQ(
          "2017/06/06 22:08:43:234, dayOfYear: 157, dayOfWeek: 3, week: 23, isoYear: 2017, "
          "isoWeek: 23, isoDayOfWeek: 2, percent: %",
          result.getValue());
  }
  
  TEST(UTCTimeAfterEpoch, DoesOutputFormatDate) {
      // Tue, Jun 6, 2017 19:38:43:234.
      auto date = Date_t::fromMillisSinceEpoch(1496777923234LL);
      std::ostringstream os;
      ASSERT_OK(TimeZoneDatabase::utcZone().outputDateWithFormat(
          os,
          "%Y/%m/%d %H:%M:%S:%L, dayOfYear: %j, "
          "dayOfWeek: %w, week: %U, isoYear: %G, "
          "isoWeek: %V, isoDayOfWeek: %u, percent: %%",
          date));
      ASSERT_EQ(os.str(),
                "2017/06/06 19:38:43:234, dayOfYear: 157, dayOfWeek: 3, week: 23, isoYear: 2017, "
                "isoWeek: 23, isoDayOfWeek: 2, percent: %");
  }
  
  TEST(NewYorkTimeAfterEpoch, DoesOutputFormatDate) {
      // 2017-06-06T19:38:43:234Z (Tuesday).
      auto date = Date_t::fromMillisSinceEpoch(1496777923234LL);
      std::ostringstream os;
      auto newYorkZone = kDefaultTimeZoneDatabase.getTimeZone("America/New_York");
      ASSERT_OK(newYorkZone.outputDateWithFormat(os,
                                                 "%Y/%m/%d %H:%M:%S:%L, dayOfYear: %j, "
                                                 "dayOfWeek: %w, week: %U, isoYear: %G, "
                                                 "isoWeek: %V, isoDayOfWeek: %u, percent: %%",
                                                 date));
      ASSERT_EQ(os.str(),
                "2017/06/06 15:38:43:234, dayOfYear: 157, dayOfWeek: 3, week: 23, isoYear: 2017, "
                "isoWeek: 23, isoDayOfWeek: 2, percent: %");
  }
  
  TEST(DateFormat, ThrowsUserExceptionIfGivenUnrecognizedFormatter) {
      ASSERT_THROWS_CODE(
          TimeZoneDatabase::utcZone().validateToStringFormat("%x"), AssertionException, 18536);
      ASSERT_THROWS_CODE(
          TimeZoneDatabase::utcZone().validateFromStringFormat("%x"), AssertionException, 18536);
  }
  
  TEST(DateFormat, ThrowsUserExceptionIfGivenUnmatchedPercent) {
      ASSERT_THROWS_CODE(
          TimeZoneDatabase::utcZone().validateToStringFormat("%"), AssertionException, 18535);
      ASSERT_THROWS_CODE(
          TimeZoneDatabase::utcZone().validateToStringFormat("%%%"), AssertionException, 18535);
      ASSERT_THROWS_CODE(
          TimeZoneDatabase::utcZone().validateToStringFormat("blahblah%"), AssertionException, 18535);
  
      // Repeat the tests with the format map for $dateFromString.
      ASSERT_THROWS_CODE(
          TimeZoneDatabase::utcZone().validateFromStringFormat("%"), AssertionException, 18535);
      ASSERT_THROWS_CODE(
          TimeZoneDatabase::utcZone().validateFromStringFormat("%%%"), AssertionException, 18535);
      ASSERT_THROWS_CODE(TimeZoneDatabase::utcZone().validateFromStringFormat("blahblah%"),
                         AssertionException,
                         18535);
  }
  
  TEST(DateFormat, ProducesNonOKStatusGivenDateBeforeYear0) {
      const long long kMillisPerYear = 31556926000;
  
      ASSERT_EQ(18537,
                TimeZoneDatabase::utcZone()
                    .formatDate("%Y", Date_t::fromMillisSinceEpoch(-(kMillisPerYear * 1971)))
                    .getStatus()
                    .code());
  
      ASSERT_EQ(18537,
                TimeZoneDatabase::utcZone()
                    .formatDate("%G", Date_t::fromMillisSinceEpoch(-(kMillisPerYear * 1971)))
                    .getStatus()
                    .code());
  
      auto result = TimeZoneDatabase::utcZone().formatDate(
          "%Y", Date_t::fromMillisSinceEpoch(-(kMillisPerYear * 1970)));
      ASSERT_OK(result);
      ASSERT_EQ("0000", result.getValue());
  }
  
  TEST(DateFormat, ProducesNonOKStatusIfGivenDateAfterYear9999) {
      ASSERT_EQ(18537,
                TimeZoneDatabase::utcZone().formatDate("%Y", Date_t::max()).getStatus().code());
  
      ASSERT_EQ(18537,
                TimeZoneDatabase::utcZone().formatDate("%G", Date_t::max()).getStatus().code());
  }
  
  TEST(DateFromString, CorrectlyParsesStringThatMatchesFormat) {
      auto input = "2017-07-04T10:56:02Z";
      auto format = "%Y-%m-%dT%H:%M:%SZ"_sd;
      auto date = kDefaultTimeZoneDatabase.fromString(input, kDefaultTimeZone, format);
      auto result = TimeZoneDatabase::utcZone().formatDate(format, date);
      ASSERT_OK(result);
      ASSERT_EQ(input, result.getValue());
  }
  
  TEST(DateFromString, RejectsStringWithInvalidYearFormat) {
      ASSERT_THROWS_CODE(kDefaultTimeZoneDatabase.fromString("201", kDefaultTimeZone, "%Y"_sd),
                         AssertionException,
                         ErrorCodes::ConversionFailure);
      ASSERT_THROWS_CODE(kDefaultTimeZoneDatabase.fromString("20i7", kDefaultTimeZone, "%Y"_sd),
                         AssertionException,
                         ErrorCodes::ConversionFailure);
  }
  
  TEST(DateFromString, RejectsStringWithInvalidMinuteFormat) {
      // Minute must be 2 digits with leading zero.
      ASSERT_THROWS_CODE(kDefaultTimeZoneDatabase.fromString(
                             "2017-01-01T00:1:00", kDefaultTimeZone, "%Y-%m-%dT%H%M%S"_sd),
                         AssertionException,
                         ErrorCodes::ConversionFailure);
      ASSERT_THROWS_CODE(kDefaultTimeZoneDatabase.fromString(
                             "2017-01-01T00:0i:00", kDefaultTimeZone, "%Y-%m-%dT%H%M%S"_sd),
                         AssertionException,
                         ErrorCodes::ConversionFailure);
  }
  
  TEST(DateFromString, RejectsStringWithInvalidSecondsFormat) {
      // Seconds must be 2 digits with leading zero.
      ASSERT_THROWS_CODE(kDefaultTimeZoneDatabase.fromString(
                             "2017-01-01T00:00:1", kDefaultTimeZone, "%Y-%m-%dT%H%M%S"_sd),
                         AssertionException,
                         ErrorCodes::ConversionFailure);
      ASSERT_THROWS_CODE(kDefaultTimeZoneDatabase.fromString(
                             "2017-01-01T00:00:i0", kDefaultTimeZone, "%Y-%m-%dT%H%M%S"_sd),
                         AssertionException,
                         ErrorCodes::ConversionFailure);
  }
  
  TEST(DateFromString, RejectsStringWithInvalidMillisecondsFormat) {
      ASSERT_THROWS_CODE(kDefaultTimeZoneDatabase.fromString(
                             "2017-01-01T00:00:00.i", kDefaultTimeZone, "%Y-%m-%dT%H:%M:%S.%L"_sd),
                         AssertionException,
                         ErrorCodes::ConversionFailure);
  }
  
  TEST(DateFromString, RejectsStringWithInvalidISOYear) {
      ASSERT_THROWS_CODE(kDefaultTimeZoneDatabase.fromString("20i7", kDefaultTimeZone, "%G"_sd),
                         AssertionException,
                         ErrorCodes::ConversionFailure);
  }
  
  TEST(DateFromString, RejectsStringWithInvalidISOWeekOfYear) {
      // ISO week of year must be between 1 and 53.
      ASSERT_THROWS_CODE(kDefaultTimeZoneDatabase.fromString("2017-55", kDefaultTimeZone, "%G-%V"_sd),
                         AssertionException,
                         ErrorCodes::ConversionFailure);
      ASSERT_THROWS_CODE(kDefaultTimeZoneDatabase.fromString("2017-FF", kDefaultTimeZone, "%G-%V"_sd),
                         AssertionException,
                         ErrorCodes::ConversionFailure);
  }
  
  TEST(DateFromString, RejectsStringWithInvalidISODayOfWeek) {
      // Day of week must be single digit between 1 and 7.
      ASSERT_THROWS_CODE(kDefaultTimeZoneDatabase.fromString("2017-8", kDefaultTimeZone, "%G-%u"_sd),
                         AssertionException,
                         ErrorCodes::ConversionFailure);
      ASSERT_THROWS_CODE(kDefaultTimeZoneDatabase.fromString("2017-0", kDefaultTimeZone, "%G-%u"_sd),
                         AssertionException,
                         ErrorCodes::ConversionFailure);
      ASSERT_THROWS_CODE(kDefaultTimeZoneDatabase.fromString("2017-a", kDefaultTimeZone, "%G-%u"_sd),
                         AssertionException,
                         ErrorCodes::ConversionFailure);
      ASSERT_THROWS_CODE(kDefaultTimeZoneDatabase.fromString("2017-11", kDefaultTimeZone, "%G-%u"_sd),
                         AssertionException,
                         ErrorCodes::ConversionFailure);
      ASSERT_THROWS_CODE(
          kDefaultTimeZoneDatabase.fromString("2017-123", kDefaultTimeZone, "%G-%u"_sd),
          AssertionException,
          ErrorCodes::ConversionFailure);
  }
  
  TEST(DateFromString, RejectsStringWithInvalidTimezoneOffset) {
      // Timezone offset minutes (%Z) requires format +/-mmm.
      ASSERT_THROWS_CODE(
          kDefaultTimeZoneDatabase.fromString("2017 500", kDefaultTimeZone, "%G %Z"_sd),
          AssertionException,
          ErrorCodes::ConversionFailure);
      ASSERT_THROWS_CODE(
          kDefaultTimeZoneDatabase.fromString("2017 0500", kDefaultTimeZone, "%G %Z"_sd),
          AssertionException,
          ErrorCodes::ConversionFailure);
      ASSERT_THROWS_CODE(
          kDefaultTimeZoneDatabase.fromString("2017 +i00", kDefaultTimeZone, "%G %Z"_sd),
          AssertionException,
          ErrorCodes::ConversionFailure);
  }
  
  TEST(DateFromString, EmptyFormatStringThrowsForAllInputs) {
      ASSERT_THROWS_CODE(kDefaultTimeZoneDatabase.fromString("1/1/2017", kDefaultTimeZone, ""_sd),
                         AssertionException,
                         ErrorCodes::ConversionFailure);
      ASSERT_THROWS_CODE(kDefaultTimeZoneDatabase.fromString("", kDefaultTimeZone, ""_sd),
                         AssertionException,
                         ErrorCodes::ConversionFailure);
  }
  
  TEST(DayOfWeek, WeekNumber) {
      long long year = 2019;
      long long week = 1;
      long long day = 1;
      long long hour = 0;
      long long minute = 0;
      long long second = 0;
      long long millisecond = 0;
  
      Date_t baseline = kDefaultTimeZone.createFromIso8601DateParts(
          year, week, day, hour, minute, second, millisecond);
  
      long long weekDurationInMillis = 7 * 24 * 60 * 60 * 1000;
  
      for (int weekIt = -10000; weekIt < 10000; weekIt++) {
          // Calculate a date using the ISO 8601 week-numbered year method.
          Date_t dateFromIso8601 = kDefaultTimeZone.createFromIso8601DateParts(
              year, weekIt, day, hour, minute, second, millisecond);
  
          // Calculate the same date by adding 'weekDurationInMillis' to 'baseline' for each week past
          // the baseline date.
          Date_t dateFromArithmetic = baseline + Milliseconds(weekDurationInMillis * (weekIt - 1));
  
          // The two methods should produce the same time.
          ASSERT_EQ(dateFromIso8601, dateFromArithmetic);
      }
  }
  
  TEST(DayOfWeek, DayNumber) {
      long long year = 2019;
      long long week = 34;
      long long day = 1;
      long long hour = 0;
      long long minute = 0;
      long long second = 0;
      long long millisecond = 0;
  
      Date_t baseline = kDefaultTimeZone.createFromIso8601DateParts(
          year, week, day, hour, minute, second, millisecond);
  
      long long dayDurationInMillis = 24 * 60 * 60 * 1000;
  
      for (int dayIt = -10000; dayIt < 10000; dayIt++) {
          // Calculate a date using the ISO 8601 week-numbered year method.
          Date_t dateFromIso8601 = kDefaultTimeZone.createFromIso8601DateParts(
              year, week, dayIt, hour, minute, second, millisecond);
  
          // Calculate the same date by adding 'dayDurationInMillis' to 'baseline' for each day past
          // the baseline date.
          Date_t dateFromArithmetic = baseline + Milliseconds(dayDurationInMillis * (dayIt - 1));
  
          // The two methods should produce the same time.
          ASSERT_EQ(dateFromIso8601, dateFromArithmetic);
      }
  }
  
  std::vector<const TimeZone*> setupTestTimezones() {
      static const std::vector<TimeZone> kAllTimezones = [&]() {
          std::vector<TimeZone> timezones{};
          for (auto&& timeZoneId : kDefaultTimeZoneDatabase.getTimeZoneStrings()) {
              timezones.push_back(kDefaultTimeZoneDatabase.getTimeZone(timeZoneId));
          }
          timezones.push_back(kDefaultTimeZoneDatabase.getTimeZone("-10:00"));
          return timezones;
      }();
      std::vector<const TimeZone*> timezones{};
      for (auto&& timezone : kAllTimezones) {
          timezones.push_back(&timezone);
      }
      return timezones;
  }
  
  // Time zones for testing.
  const TimeZone kNewYorkTimeZone = kDefaultTimeZoneDatabase.getTimeZone("America/New_York");
  const TimeZone kAustraliaEuclaTimeZone =
      kDefaultTimeZoneDatabase.getTimeZone("Australia/Eucla");  // UTC offset +08:45.
  const TimeZone kEuropeSofiaTimeZone = kDefaultTimeZoneDatabase.getTimeZone("Europe/Sofia");
  const TimeZone kAustraliaSydneyTimeZone =
      kDefaultTimeZoneDatabase.getTimeZone("Australia/Sydney");  // UTC offset +11:00.
  const TimeZone kUTCMinus10TimeZone =
      kDefaultTimeZoneDatabase.getTimeZone("-10:00");  // UTC offset -10:00.
  const TimeZone kAustraliaLordHoweTimeZone =
      kDefaultTimeZoneDatabase.getTimeZone("Australia/Lord_Howe");
  const TimeZone kEuropeMadridTimeZone = kDefaultTimeZoneDatabase.getTimeZone("Europe/Madrid");
  const std::vector<const TimeZone*> kTimezones{&kDefaultTimeZone,
                                                &kNewYorkTimeZone,
                                                &kAustraliaEuclaTimeZone,
                                                &kEuropeSofiaTimeZone,
                                                &kAustraliaSydneyTimeZone,
                                                &kUTCMinus10TimeZone};
  const std::vector<const TimeZone*> kAllTimezones{setupTestTimezones()};
  
  // Verifies 'dateDiff()' with TimeUnit::year.
  TEST(DateDiff, Year) {
      ASSERT_EQ(0,
                dateDiff(kNewYorkTimeZone.createFromDateParts(2000, 12, 31, 23, 59, 59, 500),
                         kNewYorkTimeZone.createFromDateParts(2000, 12, 31, 23, 59, 59, 999),
                         TimeUnit::year,
                         kNewYorkTimeZone));
      ASSERT_EQ(1,
                dateDiff(kNewYorkTimeZone.createFromDateParts(2000, 12, 31, 23, 59, 59, 500),
                         kNewYorkTimeZone.createFromDateParts(2001, 1, 1, 0, 0, 0, 0),
                         TimeUnit::year,
                         kNewYorkTimeZone));
      ASSERT_EQ(-1,
                dateDiff(kNewYorkTimeZone.createFromDateParts(2001, 1, 1, 0, 0, 0, 0),
                         kNewYorkTimeZone.createFromDateParts(2000, 12, 31, 23, 59, 59, 500),
                         TimeUnit::year,
                         kNewYorkTimeZone));
      ASSERT_EQ(999,
                dateDiff(kNewYorkTimeZone.createFromDateParts(1002, 1, 1, 0, 0, 0, 0),
                         kNewYorkTimeZone.createFromDateParts(2001, 1, 1, 0, 0, 0, 0),
                         TimeUnit::year,
                         kNewYorkTimeZone));
  }
  
  // Verifies 'dateDiff()' with TimeUnit::month.
  TEST(DateDiff, Month) {
      ASSERT_EQ(0,
                dateDiff(kNewYorkTimeZone.createFromDateParts(2020, 2, 28, 23, 59, 59, 500),
                         kNewYorkTimeZone.createFromDateParts(2020, 2, 29, 23, 59, 59, 999),
                         TimeUnit::month,
                         kNewYorkTimeZone));
      ASSERT_EQ(1,
                dateDiff(kNewYorkTimeZone.createFromDateParts(2020, 2, 29, 23, 59, 59, 999),
                         kNewYorkTimeZone.createFromDateParts(2020, 3, 1, 0, 0, 0, 0),
                         TimeUnit::month,
                         kNewYorkTimeZone));
      ASSERT_EQ(-14,
                dateDiff(kNewYorkTimeZone.createFromDateParts(2010, 2, 28, 23, 59, 59, 999),
                         kNewYorkTimeZone.createFromDateParts(2008, 12, 31, 23, 59, 59, 500),
                         TimeUnit::month,
                         kNewYorkTimeZone));
      ASSERT_EQ(1500 * 12,
                dateDiff(kNewYorkTimeZone.createFromDateParts(520, 3, 1, 0, 0, 0, 0),
                         kNewYorkTimeZone.createFromDateParts(2020, 3, 1, 0, 0, 0, 0),
                         TimeUnit::month,
                         kNewYorkTimeZone));
  }
  
  // Verifies 'dateDiff()' with TimeUnit::quarter.
  TEST(DateDiff, Quarter) {
      ASSERT_EQ(0,
                dateDiff(kNewYorkTimeZone.createFromDateParts(2020, 1, 1, 0, 0, 0, 0),
                         kNewYorkTimeZone.createFromDateParts(2020, 3, 31, 23, 59, 59, 999),
                         TimeUnit::quarter,
                         kNewYorkTimeZone));
      ASSERT_EQ(1,
                dateDiff(kNewYorkTimeZone.createFromDateParts(2020, 1, 1, 0, 0, 0, 0),
                         kNewYorkTimeZone.createFromDateParts(2020, 4, 1, 0, 0, 0, 0),
                         TimeUnit::quarter,
                         kNewYorkTimeZone));
      ASSERT_EQ(-2001,
                dateDiff(kNewYorkTimeZone.createFromDateParts(2000, 12, 31, 23, 59, 59, 500),
                         kNewYorkTimeZone.createFromDateParts(1500, 9, 30, 23, 59, 59, 999),
                         TimeUnit::quarter,
                         kNewYorkTimeZone));
  }
  
  // Verifies 'dateDiff()' with TimeUnit::week.
  TEST(DateDiff, Week) {
      // Cases when the first day of the week is Monday.
      ASSERT_EQ(1,
                dateDiff(kNewYorkTimeZone.createFromDateParts(2020, 11, 2, 0, 0, 0, 0),
                         kNewYorkTimeZone.createFromDateParts(2020, 11, 9, 0, 0, 0, 0),
                         TimeUnit::week,
                         kNewYorkTimeZone,
                         DayOfWeek::monday));
      ASSERT_EQ(1,
                dateDiff(kNewYorkTimeZone.createFromDateParts(2020, 11, 2, 0, 0, 0, 0),
                         kNewYorkTimeZone.createFromDateParts(2020, 11, 15, 0, 0, 0, 0),
                         TimeUnit::week,
                         kNewYorkTimeZone,
                         DayOfWeek::monday));
      ASSERT_EQ(0,
                dateDiff(kNewYorkTimeZone.createFromDateParts(2020, 11, 2, 0, 0, 0, 0),
                         kNewYorkTimeZone.createFromDateParts(2020, 11, 8, 0, 0, 0, 0),
                         TimeUnit::week,
                         kNewYorkTimeZone,
                         DayOfWeek::monday));
      ASSERT_EQ(0,
                dateDiff(kNewYorkTimeZone.createFromDateParts(2020, 11, 2, 0, 0, 0, 0),
                         kNewYorkTimeZone.createFromDateParts(2020, 11, 2, 0, 0, 0, 0),
                         TimeUnit::week,
                         kNewYorkTimeZone,
                         DayOfWeek::monday));
      ASSERT_EQ(0,
                dateDiff(kNewYorkTimeZone.createFromDateParts(2020, 11, 2, 0, 0, 0, 0),
                         kNewYorkTimeZone.createFromDateParts(2020, 11, 3, 0, 0, 0, 0),
                         TimeUnit::week,
                         kNewYorkTimeZone,
                         DayOfWeek::monday));
      ASSERT_EQ(1,
                dateDiff(kNewYorkTimeZone.createFromDateParts(2020, 11, 8, 0, 0, 0, 0),
                         kNewYorkTimeZone.createFromDateParts(2020, 11, 9, 0, 0, 0, 0),
                         TimeUnit::week,
                         kNewYorkTimeZone,
                         DayOfWeek::monday));
      ASSERT_EQ(1,
                dateDiff(kNewYorkTimeZone.createFromDateParts(2020, 11, 8, 0, 0, 0, 0),
                         kNewYorkTimeZone.createFromDateParts(2020, 11, 15, 0, 0, 0, 0),
                         TimeUnit::week,
                         kNewYorkTimeZone,
                         DayOfWeek::monday));
      ASSERT_EQ(-5,
                dateDiff(kNewYorkTimeZone.createFromDateParts(2020, 11, 10, 0, 0, 0, 0),
                         kNewYorkTimeZone.createFromDateParts(2020, 10, 8, 0, 0, 0, 0),
                         TimeUnit::week,
                         kNewYorkTimeZone,
                         DayOfWeek::monday));
  
      // Cases when the first day of the week is not Monday.
      ASSERT_EQ(1,
                dateDiff(kNewYorkTimeZone.createFromDateParts(2020, 11, 13, 0, 0, 0, 0),  // Friday.
                         kNewYorkTimeZone.createFromDateParts(2020, 11, 15, 0, 0, 0, 0),  // Sunday.
                         TimeUnit::week,
                         kNewYorkTimeZone,
                         DayOfWeek::sunday));
      ASSERT_EQ(0,
                dateDiff(kNewYorkTimeZone.createFromDateParts(2020, 11, 13, 0, 0, 0, 0),  // Friday.
                         kNewYorkTimeZone.createFromDateParts(2020, 11, 14, 0, 0, 0, 0),  // Saturday.
                         TimeUnit::week,
                         kNewYorkTimeZone,
                         DayOfWeek::sunday));
      ASSERT_EQ(1,
                dateDiff(kNewYorkTimeZone.createFromDateParts(2020, 11, 8, 0, 0, 0, 0),   // Sunday.
                         kNewYorkTimeZone.createFromDateParts(2020, 11, 15, 0, 0, 0, 0),  // Sunday.
                         TimeUnit::week,
                         kNewYorkTimeZone,
                         DayOfWeek::sunday));
      ASSERT_EQ(2,
                dateDiff(kNewYorkTimeZone.createFromDateParts(2020, 11, 7, 0, 0, 0, 0),   // Saturday.
                         kNewYorkTimeZone.createFromDateParts(2020, 11, 15, 0, 0, 0, 0),  // Sunday.
                         TimeUnit::week,
                         kNewYorkTimeZone,
                         DayOfWeek::sunday));
      ASSERT_EQ(0,
                dateDiff(kNewYorkTimeZone.createFromDateParts(2020, 11, 4, 0, 0, 0, 0),  // Wednesday.
                         kNewYorkTimeZone.createFromDateParts(2020, 11, 10, 0, 0, 0, 0),  // Tuesday.
                         TimeUnit::week,
                         kNewYorkTimeZone,
                         DayOfWeek::wednesday));
  }
  
  // Verifies 'dateDiff()' with TimeUnit::day.
  TEST(DateDiff, Day) {
      ASSERT_EQ(0,
                dateDiff(kNewYorkTimeZone.createFromDateParts(2020, 11, 8, 0, 0, 0, 0),
                         kNewYorkTimeZone.createFromDateParts(2020, 11, 8, 23, 59, 59, 999),
                         TimeUnit::day,
                         kNewYorkTimeZone));
      ASSERT_EQ(1,
                dateDiff(kNewYorkTimeZone.createFromDateParts(2020, 11, 8, 0, 0, 0, 0),
                         kNewYorkTimeZone.createFromDateParts(2020, 11, 9, 0, 0, 0, 0),
                         TimeUnit::day,
                         kNewYorkTimeZone));
      ASSERT_EQ(-1,
                dateDiff(kNewYorkTimeZone.createFromDateParts(2020, 11, 9, 0, 0, 0, 0),
                         kNewYorkTimeZone.createFromDateParts(2020, 11, 8, 23, 59, 59, 999),
                         TimeUnit::day,
                         kNewYorkTimeZone));
  
      // Verifies number of days in a year calculation.
      ASSERT_EQ(369,
                dateDiff(kNewYorkTimeZone.createFromDateParts(1999, 12, 30, 0, 0, 0, 0),
                         kNewYorkTimeZone.createFromDateParts(2001, 1, 2, 0, 0, 0, 0),
                         TimeUnit::day,
                         kNewYorkTimeZone));
      ASSERT_EQ(6575,
                dateDiff(kNewYorkTimeZone.createFromDateParts(1583, 1, 1, 0, 0, 0, 0),
                         kNewYorkTimeZone.createFromDateParts(1601, 1, 1, 0, 0, 0, 0),
                         TimeUnit::day,
                         kNewYorkTimeZone));
      ASSERT_EQ(-6575,
                dateDiff(kNewYorkTimeZone.createFromDateParts(1601, 1, 1, 0, 0, 0, 0),
                         kNewYorkTimeZone.createFromDateParts(1583, 1, 1, 0, 0, 0, 0),
                         TimeUnit::day,
                         kNewYorkTimeZone));
      ASSERT_EQ(29,
                dateDiff(kNewYorkTimeZone.createFromDateParts(2004, 2, 10, 0, 0, 0, 0),
                         kNewYorkTimeZone.createFromDateParts(2004, 3, 10, 0, 0, 0, 0),
                         TimeUnit::day,
                         kNewYorkTimeZone));
      ASSERT_EQ(28,
                dateDiff(kAustraliaEuclaTimeZone.createFromDateParts(2005, 2, 10, 0, 0, 0, 0),
                         kAustraliaEuclaTimeZone.createFromDateParts(2005, 3, 10, 0, 0, 0, 0),
                         TimeUnit::day,
                         kAustraliaEuclaTimeZone));
  
      // Use timelib_day_of_year as an oracle to verify day calculations.
      for (int year = -1000; year < 3000; ++year) {
          int expectedNumberOfDays = timelib_day_of_year(year, 12, 31) + 1;
          ASSERT_EQ(expectedNumberOfDays,
                    dateDiff(kNewYorkTimeZone.createFromDateParts(year, 2, 3, 0, 0, 0, 0),
                             kNewYorkTimeZone.createFromDateParts(year + 1, 2, 3, 0, 0, 0, 0),
                             TimeUnit::day,
                             kNewYorkTimeZone));
      }
  }
  
  // Verifies 'dateDiff()' with TimeUnit::hour.
  TEST(DateDiff, Hour) {
      ASSERT_EQ(0,
                dateDiff(kNewYorkTimeZone.createFromDateParts(2020, 11, 8, 1, 0, 0, 0),
                         kNewYorkTimeZone.createFromDateParts(2020, 11, 8, 1, 59, 59, 999),
                         TimeUnit::hour,
                         kNewYorkTimeZone));
      ASSERT_EQ(1,
                dateDiff(kNewYorkTimeZone.createFromDateParts(2020, 11, 8, 1, 59, 59, 999),
                         kNewYorkTimeZone.createFromDateParts(2020, 11, 8, 2, 0, 0, 0),
                         TimeUnit::hour,
                         kNewYorkTimeZone));
      ASSERT_EQ(-25,
                dateDiff(kNewYorkTimeZone.createFromDateParts(2020, 11, 10, 0, 0, 0, 0),
                         kNewYorkTimeZone.createFromDateParts(2020, 11, 8, 23, 59, 59, 999),
                         TimeUnit::hour,
                         kNewYorkTimeZone));
  
      // Hour difference calculation in UTC offset +08:45 time zone.
      ASSERT_EQ(
          1,
          dateDiff(
              kAustraliaEuclaTimeZone.createFromDateParts(2020, 11, 10, 20, 55, 0, 0) /*UTC 12:10*/,
              kAustraliaEuclaTimeZone.createFromDateParts(2020, 11, 10, 21, 5, 0, 0) /*UTC 12:20*/,
              TimeUnit::hour,
              kAustraliaEuclaTimeZone));
  
      // Test of transition from DST to standard time.
      ASSERT_EQ(1,
                dateDiff(kDefaultTimeZone.createFromDateParts(
                             2020, 11, 1, 5, 0, 0, 0) /* America/New_York 1:00AM EDT (UTC-4)*/,
                         kDefaultTimeZone.createFromDateParts(
                             2020, 11, 1, 6, 0, 0, 0) /* America/New_York 1:00AM EST (UTC-5)*/,
                         TimeUnit::hour,
                         kNewYorkTimeZone));
      ASSERT_EQ(-1,
                dateDiff(kDefaultTimeZone.createFromDateParts(
                             2020, 11, 1, 6, 0, 0, 0) /* America/New_York 1:00AM EST (UTC-5)*/,
                         kDefaultTimeZone.createFromDateParts(
                             2020, 11, 1, 5, 0, 0, 0) /* America/New_York 1:00AM EDT (UTC-4)*/,
                         TimeUnit::hour,
                         kNewYorkTimeZone));
  
      // Test of transition from standard time to DST.
      ASSERT_EQ(1,
                dateDiff(kDefaultTimeZone.createFromDateParts(
                             2020, 3, 8, 6, 45, 0, 0) /* America/New_York 1:45AM EST (UTC-5)*/,
                         kDefaultTimeZone.createFromDateParts(
                             2020, 3, 8, 7, 0, 0, 0) /* America/New_York 3:00AM EDT (UTC-4)*/,
                         TimeUnit::hour,
                         kNewYorkTimeZone));
      ASSERT_EQ(-1,
                dateDiff(kDefaultTimeZone.createFromDateParts(
                             2020, 3, 8, 7, 0, 0, 0) /* America/New_York 3:00AM EDT (UTC-4)*/,
                         kDefaultTimeZone.createFromDateParts(
                             2020, 3, 8, 6, 45, 0, 0) /* America/New_York 1:45AM EST (UTC-5)*/,
                         TimeUnit::hour,
                         kNewYorkTimeZone));
  
      // Longer period test.
      ASSERT_EQ(17545,
                dateDiff(kNewYorkTimeZone.createFromDateParts(1999, 1, 1, 0, 0, 0, 0),
                         kNewYorkTimeZone.createFromDateParts(2001, 1, 1, 1, 0, 0, 0),
                         TimeUnit::hour,
                         kNewYorkTimeZone));
  
      // Tests with Australia/Lord_Howe time zone that has 00:30 hour Daylight Savings Time (DST) UTC
      // offset change. 'startDate' and 'endDate' parameters span a transition from/to DST.
      //
      // Verify that even when the UTC offset change is 30 minutes on transition from DST to Standard
      // Time, time difference in hours is based on the local time. In the test 1.5h of real time
      // passes, but the returned difference is 1h.
      ASSERT_EQ(1,
                dateDiff(kAustraliaLordHoweTimeZone.createFromDateParts(2021, 4, 4, 1, 0, 0, 0),
                         kAustraliaLordHoweTimeZone.createFromDateParts(2021, 4, 4, 2, 0, 0, 0),
                         TimeUnit::hour,
                         kAustraliaLordHoweTimeZone));
  
      // Verify that even when the UTC offset change is 30 minutes on transition from Standard Time to
      // DST, time difference in hours is based on the local time. In the test 1h of real time passes
      // and the returned difference is 1h.
      ASSERT_EQ(1,
                dateDiff(kAustraliaLordHoweTimeZone.createFromDateParts(2021, 10, 3, 1, 0, 0, 0),
                         kAustraliaLordHoweTimeZone.createFromDateParts(2021, 10, 3, 2, 30, 0, 0),
                         TimeUnit::hour,
                         kAustraliaLordHoweTimeZone));
  }
  
  // Verifies 'dateDiff()' with TimeUnit::minute.
  TEST(DateDiff, Minute) {
      ASSERT_EQ(0,
                dateDiff(kNewYorkTimeZone.createFromDateParts(2020, 11, 8, 1, 30, 0, 0),
                         kNewYorkTimeZone.createFromDateParts(2020, 11, 8, 1, 30, 59, 999),
                         TimeUnit::minute,
                         kNewYorkTimeZone));
      ASSERT_EQ(1,
                dateDiff(kNewYorkTimeZone.createFromDateParts(2020, 11, 8, 1, 30, 59, 999),
                         kNewYorkTimeZone.createFromDateParts(2020, 11, 8, 1, 31, 0, 0),
                         TimeUnit::minute,
                         kNewYorkTimeZone));
      ASSERT_EQ(-25,
                dateDiff(kNewYorkTimeZone.createFromDateParts(2020, 11, 8, 1, 55, 0, 0),
                         kNewYorkTimeZone.createFromDateParts(2020, 11, 8, 1, 30, 59, 999),
                         TimeUnit::minute,
                         kNewYorkTimeZone));
      ASSERT_EQ(234047498,
                dateDiff(kNewYorkTimeZone.createFromDateParts(1585, 11, 8, 1, 55, 0, 0),
                         kNewYorkTimeZone.createFromDateParts(2030, 11, 8, 1, 30, 59, 999),
                         TimeUnit::minute,
                         kNewYorkTimeZone));
  
      // Tests with Australia/Lord_Howe time zone that has 00:30 hour Daylight Savings Time (DST) UTC
      // offset change. 'startDate' and 'endDate' parameters span a transition from/to DST.
      ASSERT_EQ(90,
                dateDiff(kAustraliaLordHoweTimeZone.createFromDateParts(
                             2021, 4, 4, 1, 0, 0, 0),  // UTC 2021-04-03T14:00:00
                         kAustraliaLordHoweTimeZone.createFromDateParts(
                             2021, 4, 4, 2, 0, 0, 0),  // UTC 2021-04-03T15:30:00
                         TimeUnit::minute,
                         kAustraliaLordHoweTimeZone));
      ASSERT_EQ(60,
                dateDiff(kAustraliaLordHoweTimeZone.createFromDateParts(
                             2021, 10, 3, 1, 0, 0, 0),  // UTC 2021-10-02T14:30:00
                         kAustraliaLordHoweTimeZone.createFromDateParts(
                             2021, 10, 3, 2, 30, 0, 0),  // UTC 2021-10-02T15:30:00
                         TimeUnit::minute,
                         kAustraliaLordHoweTimeZone));
  }
  
  // Verifies 'dateDiff()' with TimeUnit::second.
  TEST(DateDiff, Second) {
      ASSERT_EQ(0,
                dateDiff(kNewYorkTimeZone.createFromDateParts(2020, 11, 8, 1, 30, 15, 0),
                         kNewYorkTimeZone.createFromDateParts(2020, 11, 8, 1, 30, 15, 999),
                         TimeUnit::second,
                         kNewYorkTimeZone));
      ASSERT_EQ(1,
                dateDiff(kNewYorkTimeZone.createFromDateParts(2020, 11, 8, 1, 30, 15, 999),
                         kNewYorkTimeZone.createFromDateParts(2020, 11, 8, 1, 30, 16, 0),
                         TimeUnit::second,
                         kNewYorkTimeZone));
      ASSERT_EQ(-2401,
                dateDiff(kNewYorkTimeZone.createFromDateParts(2020, 11, 8, 2, 10, 16, 999),
                         kNewYorkTimeZone.createFromDateParts(2020, 11, 8, 1, 30, 15, 999),
                         TimeUnit::second,
                         kNewYorkTimeZone));
      ASSERT_EQ(1604971816,
                dateDiff(kDefaultTimeZone.createFromDateParts(1970, 1, 1, 0, 0, 0, 0),
                         kDefaultTimeZone.createFromDateParts(2020, 11, 10, 1, 30, 16, 0),
                         TimeUnit::second,
                         kNewYorkTimeZone));
  
      // Verify that negative milliseconds from the Unix Epoch are properly handled.
      ASSERT_EQ(2,
                dateDiff(kDefaultTimeZone.createFromDateParts(1969, 12, 31, 23, 59, 59, 999),
                         kDefaultTimeZone.createFromDateParts(1970, 1, 1, 0, 0, 1, 0),
                         TimeUnit::second,
                         kDefaultTimeZone));
  
      // Tests with Australia/Lord_Howe time zone that has 00:30 hour Daylight Savings Time (DST) UTC
      // offset change. 'startDate' and 'endDate' parameters span a transition from/to DST.
      const int secondsPerMinute{60};
      ASSERT_EQ(90 * secondsPerMinute,
                dateDiff(kAustraliaLordHoweTimeZone.createFromDateParts(
                             2021, 4, 4, 1, 0, 0, 0),  // UTC 2021-04-03T14:00:00
                         kAustraliaLordHoweTimeZone.createFromDateParts(
                             2021, 4, 4, 2, 0, 0, 0),  // UTC 2021-04-03T15:30:00
                         TimeUnit::second,
                         kAustraliaLordHoweTimeZone));
      ASSERT_EQ(60 * secondsPerMinute,
                dateDiff(kAustraliaLordHoweTimeZone.createFromDateParts(
                             2021, 10, 3, 1, 0, 0, 0),  // UTC 2021-10-02T14:30:00
                         kAustraliaLordHoweTimeZone.createFromDateParts(
                             2021, 10, 3, 2, 30, 0, 0),  // UTC 2021-10-02T15:30:00
                         TimeUnit::second,
                         kAustraliaLordHoweTimeZone));
  
      // Verify that UTC offset adjustments are properly accounted for when calculating the time
      // difference. Time zone Europe/Madrid skips 0:14:44 hours at 1900-12-31 23:45:15 to change the
      // timezone to UTC.
      ASSERT_EQ(1,
                dateDiff(kEuropeMadridTimeZone.createFromDateParts(1900, 12, 31, 23, 45, 15, 0),
                         kEuropeMadridTimeZone.createFromDateParts(1901, 1, 1, 0, 0, 0, 0),
                         TimeUnit::second,
                         kEuropeMadridTimeZone));
  }
  
  // Verifies 'dateDiff()' with TimeUnit::millisecond.
  TEST(DateDiff, Millisecond) {
      ASSERT_EQ(100,
                dateDiff(kNewYorkTimeZone.createFromDateParts(2020, 11, 8, 1, 30, 15, 0),
                         kNewYorkTimeZone.createFromDateParts(2020, 11, 8, 1, 30, 15, 100),
                         TimeUnit::millisecond,
                         kNewYorkTimeZone));
      ASSERT_EQ(-1500,
                dateDiff(kNewYorkTimeZone.createFromDateParts(2020, 11, 8, 1, 30, 16, 500),
                         kNewYorkTimeZone.createFromDateParts(2020, 11, 8, 1, 30, 15, 0),
                         TimeUnit::millisecond,
                         kNewYorkTimeZone));
      ASSERT_EQ(1604971816000,
                dateDiff(kDefaultTimeZone.createFromDateParts(1970, 1, 1, 0, 0, 0, 0),
                         kDefaultTimeZone.createFromDateParts(2020, 11, 10, 1, 30, 16, 0),
                         TimeUnit::millisecond,
                         kNewYorkTimeZone));
  
      // Verifies numeric overflow handling.
      ASSERT_THROWS_CODE(dateDiff(Date_t::fromMillisSinceEpoch(std::numeric_limits<long long>::min()),
                                  Date_t::fromMillisSinceEpoch(std::numeric_limits<long long>::max()),
                                  TimeUnit::millisecond,
                                  kNewYorkTimeZone),
                         AssertionException,
                         5166308);
  }
  
  // Verifies 'truncateDate()' with TimeUnit::millisecond.
  TEST(TruncateDate, Millisecond) {
      for (auto* timezone : kTimezones) {
          // Value in the middle of a bin above default reference point.
          ASSERT_EQ(timezone->createFromDateParts(2000, 1, 1, 0, 0, 1, 20),
                    truncateDate(timezone->createFromDateParts(2000, 1, 1, 0, 0, 2, 0),
                                 TimeUnit::millisecond,
                                 1020,
                                 *timezone));
  
          // Value at the lower boundary of a bin.
          ASSERT_EQ(timezone->createFromDateParts(2000, 1, 1, 0, 0, 1, 20),
                    truncateDate(timezone->createFromDateParts(2000, 1, 1, 0, 0, 1, 20),
                                 TimeUnit::millisecond,
                                 1020,
                                 *timezone));
  
          // Value at the top of a bin.
          ASSERT_EQ(timezone->createFromDateParts(2000, 1, 1, 0, 0, 1, 20),
                    truncateDate(timezone->createFromDateParts(2000, 1, 1, 0, 0, 2, 39),
                                 TimeUnit::millisecond,
                                 1020,
                                 *timezone));
  
          // Value at the bottom of the next bin.
          ASSERT_EQ(timezone->createFromDateParts(2000, 1, 1, 0, 0, 2, 40),
                    truncateDate(timezone->createFromDateParts(2000, 1, 1, 0, 0, 2, 40),
                                 TimeUnit::millisecond,
                                 1020,
                                 *timezone));
  
          // Value at the top of a bin below default reference point.
          ASSERT_EQ(timezone->createFromDateParts(1999, 12, 31, 23, 59, 58, 500),
                    truncateDate(timezone->createFromDateParts(1999, 12, 31, 23, 59, 59, 999),
                                 TimeUnit::millisecond,
                                 1500,
                                 *timezone));
      }
  }
  
  // Verifies 'truncateDate()' with TimeUnit::second.
  TEST(TruncateDate, Second) {
      for (auto* timezone : kTimezones) {
          // Value in the middle of a bin above default reference point.
          ASSERT_EQ(timezone->createFromDateParts(2000, 1, 1, 0, 0, 5, 0),
                    truncateDate(timezone->createFromDateParts(2000, 1, 1, 0, 0, 9, 0),
                                 TimeUnit::second,
                                 5,
                                 *timezone));
  
          // Value at the lower boundary of a bin.
          ASSERT_EQ(timezone->createFromDateParts(2000, 1, 1, 0, 0, 5, 0),
                    truncateDate(timezone->createFromDateParts(2000, 1, 1, 0, 0, 5, 0),
                                 TimeUnit::second,
                                 5,
                                 *timezone));
  
          // Value at the top of a bin.
          ASSERT_EQ(timezone->createFromDateParts(2000, 1, 1, 0, 0, 5, 0),
                    truncateDate(timezone->createFromDateParts(2000, 1, 1, 0, 0, 9, 999),
                                 TimeUnit::second,
                                 5,
                                 *timezone));
  
          // Value at the bottom of the next bin.
          ASSERT_EQ(timezone->createFromDateParts(2000, 1, 1, 0, 0, 10, 0),
                    truncateDate(timezone->createFromDateParts(2000, 1, 1, 0, 0, 10, 0),
                                 TimeUnit::second,
                                 5,
                                 *timezone));
  
          // Value at the top of a bin below default reference point.
          ASSERT_EQ(timezone->createFromDateParts(1999, 12, 31, 23, 59, 50, 0),
                    truncateDate(timezone->createFromDateParts(1999, 12, 31, 23, 59, 51, 0),
                                 TimeUnit::second,
                                 5,
                                 *timezone));
      }
  }
  
  // Verifies 'truncateDate()' with TimeUnit::minute.
  TEST(TruncateDate, Minute) {
      for (auto* timezone : kTimezones) {
          // Value in the middle of a bin above default reference point.
          ASSERT_EQ(timezone->createFromDateParts(2000, 1, 1, 0, 5, 0, 0),
                    truncateDate(timezone->createFromDateParts(2000, 1, 1, 0, 9, 0, 0),
                                 TimeUnit::minute,
                                 5,
                                 *timezone));
  
          // Value at the lower boundary of a bin.
          ASSERT_EQ(timezone->createFromDateParts(2000, 1, 1, 0, 5, 0, 0),
                    truncateDate(timezone->createFromDateParts(2000, 1, 1, 0, 5, 0, 0),
                                 TimeUnit::minute,
                                 5,
                                 *timezone));
  
          // Value at the top of a bin.
          ASSERT_EQ(timezone->createFromDateParts(2000, 1, 1, 0, 5, 0, 0),
                    truncateDate(timezone->createFromDateParts(2000, 1, 1, 0, 9, 59, 999),
                                 TimeUnit::minute,
                                 5,
                                 *timezone));
  
          // Value at the bottom of the next bin.
          ASSERT_EQ(timezone->createFromDateParts(2000, 1, 1, 0, 10, 0, 0),
                    truncateDate(timezone->createFromDateParts(2000, 1, 1, 0, 10, 10, 0),
                                 TimeUnit::minute,
                                 5,
                                 *timezone));
  
          // Value at the top of a bin below default reference point.
          ASSERT_EQ(timezone->createFromDateParts(1999, 12, 31, 23, 50, 0, 0),
                    truncateDate(timezone->createFromDateParts(1999, 12, 31, 23, 54, 0, 0),
                                 TimeUnit::minute,
                                 5,
                                 *timezone));
      }
  }
  
  // Verifies 'truncateDate()' with TimeUnit::hour.
  TEST(TruncateDate, Hour) {
      for (auto* timezone : kTimezones) {
          // Value in the middle of a bin above default reference point.
          ASSERT_EQ(timezone->createFromDateParts(2020, 1, 1, 2, 0, 0, 0),
                    truncateDate(timezone->createFromDateParts(2020, 1, 1, 3, 0, 0, 0),
                                 TimeUnit::hour,
                                 2,
                                 *timezone));
  
          // Value at the lower boundary of a bin.
          ASSERT_EQ(timezone->createFromDateParts(2020, 1, 1, 2, 0, 0, 0),
                    truncateDate(timezone->createFromDateParts(2020, 1, 1, 2, 0, 0, 0),
                                 TimeUnit::hour,
                                 2,
                                 *timezone));
  
          // Value at the top of a bin.
          ASSERT_EQ(timezone->createFromDateParts(2020, 1, 1, 2, 0, 0, 0),
                    truncateDate(timezone->createFromDateParts(2020, 1, 1, 3, 59, 59, 999),
                                 TimeUnit::hour,
                                 2,
                                 *timezone));
  
          // Value at the bottom of the next bin.
          ASSERT_EQ(timezone->createFromDateParts(2020, 1, 1, 4, 0, 0, 0),
                    truncateDate(timezone->createFromDateParts(2020, 1, 1, 4, 0, 0, 0),
                                 TimeUnit::hour,
                                 2,
                                 *timezone));
  
          // Value at the top of a bin below default reference point.
          ASSERT_EQ(timezone->createFromDateParts(1999, 12, 31, 22, 0, 0, 0),
                    truncateDate(timezone->createFromDateParts(1999, 12, 31, 23, 59, 51, 0),
                                 TimeUnit::hour,
                                 2,
                                 *timezone));
      }
  }
  
  // Verifies 'truncateDate()' with TimeUnit::day.
  TEST(TruncateDate, Day) {
      for (auto* timezone : kAllTimezones) {
          // Value in the middle of a bin above default reference point.
          ASSERT_EQ(timezone->createFromDateParts(2000, 1, 7, 0, 0, 0, 0),
                    truncateDate(timezone->createFromDateParts(2000, 1, 8, 0, 0, 2, 0),
                                 TimeUnit::day,
                                 2,
                                 *timezone));
  
          // Value at the lower boundary of a bin.
          ASSERT_EQ(timezone->createFromDateParts(2000, 1, 9, 0, 0, 0, 0),
                    truncateDate(timezone->createFromDateParts(2000, 1, 9, 0, 0, 0, 0),
                                 TimeUnit::day,
                                 2,
                                 *timezone));
  
          // Value at the top of a bin.
          ASSERT_EQ(timezone->createFromDateParts(2000, 1, 9, 0, 0, 0, 0),
                    truncateDate(timezone->createFromDateParts(2000, 1, 10, 23, 59, 59, 999),
                                 TimeUnit::day,
                                 2,
                                 *timezone));
  
          // Value at the top of a bin below default reference point.
          ASSERT_EQ(timezone->createFromDateParts(1999, 12, 28, 0, 0, 0, 0),
                    truncateDate(timezone->createFromDateParts(1999, 12, 29, 23, 59, 59, 999),
                                 TimeUnit::day,
                                 2,
                                 *timezone));
      }
  
      // Daylight Savings Time tests with America/New_York timezone.
      // Day before DST is on.
      ASSERT_EQ(kNewYorkTimeZone.createFromDateParts(2021, 3, 13, 0, 0, 0, 0),
                truncateDate(kNewYorkTimeZone.createFromDateParts(2021, 3, 13, 4, 0, 0, 0),
                             TimeUnit::day,
                             1,
                             kNewYorkTimeZone));
  
      // Day when DST is on.
      ASSERT_EQ(kNewYorkTimeZone.createFromDateParts(2021, 3, 14, 0, 0, 0, 0),
                truncateDate(kNewYorkTimeZone.createFromDateParts(2021, 3, 14, 4, 0, 0, 0),
                             TimeUnit::day,
                             1,
                             kNewYorkTimeZone));
  
      // The next day when DST is on.
      ASSERT_EQ(kNewYorkTimeZone.createFromDateParts(2021, 3, 15, 0, 0, 0, 0),
                truncateDate(kNewYorkTimeZone.createFromDateParts(2021, 3, 15, 1, 0, 0, 0),
                             TimeUnit::day,
                             1,
                             kNewYorkTimeZone));
  
      // The last day when DST is on.
      ASSERT_EQ(kNewYorkTimeZone.createFromDateParts(2021, 11, 7, 0, 0, 0, 0),
                truncateDate(kNewYorkTimeZone.createFromDateParts(2021, 11, 7, 0, 30, 0, 0),
                             TimeUnit::day,
                             1,
                             kNewYorkTimeZone));
  
      // The DST is off.
      ASSERT_EQ(kNewYorkTimeZone.createFromDateParts(2021, 11, 7, 0, 0, 0, 0),
                truncateDate(kNewYorkTimeZone.createFromDateParts(2021, 11, 7, 2, 0, 0, 0),
                             TimeUnit::day,
                             1,
                             kNewYorkTimeZone));
  
      // The next day DST is off.
      ASSERT_EQ(kNewYorkTimeZone.createFromDateParts(2021, 11, 8, 0, 0, 0, 0),
                truncateDate(kNewYorkTimeZone.createFromDateParts(2021, 11, 8, 2, 0, 0, 0),
                             TimeUnit::day,
                             1,
                             kNewYorkTimeZone));
  
  
      // Daylight Savings Time tests with Europe/Sofia timezone.
      // Day before DST is on.
      ASSERT_EQ(kEuropeSofiaTimeZone.createFromDateParts(2021, 3, 27, 0, 0, 0, 0),
                truncateDate(kEuropeSofiaTimeZone.createFromDateParts(2021, 3, 27, 22, 0, 0, 0),
                             TimeUnit::day,
                             1,
                             kEuropeSofiaTimeZone));
  
      // Day when DST is on.
      ASSERT_EQ(kEuropeSofiaTimeZone.createFromDateParts(2021, 3, 28, 0, 0, 0, 0),
                truncateDate(kEuropeSofiaTimeZone.createFromDateParts(2021, 3, 28, 4, 0, 0, 0),
                             TimeUnit::day,
                             1,
                             kEuropeSofiaTimeZone));
  
      // The next day when DST is on.
      ASSERT_EQ(kEuropeSofiaTimeZone.createFromDateParts(2021, 3, 29, 0, 0, 0, 0),
                truncateDate(kEuropeSofiaTimeZone.createFromDateParts(2021, 3, 29, 1, 0, 0, 0),
                             TimeUnit::day,
                             1,
                             kEuropeSofiaTimeZone));
  
      // The last day when DST is on.
      ASSERT_EQ(kEuropeSofiaTimeZone.createFromDateParts(2021, 10, 31, 0, 0, 0, 0),
                truncateDate(kEuropeSofiaTimeZone.createFromDateParts(2021, 10, 31, 0, 30, 0, 0),
                             TimeUnit::day,
                             1,
                             kEuropeSofiaTimeZone));
  
      // The DST is off.
      ASSERT_EQ(kEuropeSofiaTimeZone.createFromDateParts(2021, 11, 1, 0, 0, 0, 0),
                truncateDate(kEuropeSofiaTimeZone.createFromDateParts(2021, 11, 1, 0, 0, 0, 1),
                             TimeUnit::day,
                             1,
                             kEuropeSofiaTimeZone));
  }
  
  // Verifies 'truncateDate()' with TimeUnit::week.
  TEST(TruncateDate, Week) {
      for (auto* timezone : kAllTimezones) {
          // Value in the middle of a bin above default reference point. The first day of a week is
          // Wednesday.
          ASSERT_EQ(timezone->createFromDateParts(2000, 1, 19, 0, 0, 0, 0),
                    truncateDate(timezone->createFromDateParts(2000, 1, 26, 0, 0, 0, 0),
                                 TimeUnit::week,
                                 2,
                                 *timezone,
                                 DayOfWeek::wednesday));
  
          // Reference point coincides with start of the week (Saturday).
          ASSERT_EQ(timezone->createFromDateParts(2000, 1, 1, 0, 0, 0, 0),
                    truncateDate(timezone->createFromDateParts(2000, 1, 2, 0, 0, 0, 0),
                                 TimeUnit::week,
                                 1,
                                 *timezone,
                                 DayOfWeek::saturday));
  
          // Value at the lower boundary of a bin. The first day of a week is Monday.
          ASSERT_EQ(timezone->createFromDateParts(2021, 7, 26, 0, 0, 0, 0),
                    truncateDate(timezone->createFromDateParts(2021, 7, 26, 0, 0, 0, 0),
                                 TimeUnit::week,
                                 1,
                                 *timezone,
                                 DayOfWeek::monday));
  
          // Value at the top of a bin. The first day of a week is Monday.
          ASSERT_EQ(timezone->createFromDateParts(2021, 7, 26, 0, 0, 0, 0),
                    truncateDate(timezone->createFromDateParts(2021, 8, 1, 23, 59, 59, 999),
                                 TimeUnit::week,
                                 1,
                                 *timezone,
                                 DayOfWeek::monday));
  
          // Value at the top of a bin below default reference point. The first day of a week is
          // Monday.
          ASSERT_EQ(timezone->createFromDateParts(1999, 12, 6, 0, 0, 0, 0),
                    truncateDate(timezone->createFromDateParts(1999, 12, 19, 23, 59, 59, 999),
                                 TimeUnit::week,
                                 2,
                                 *timezone,
                                 DayOfWeek::monday));
      }
  }
  
  // Verifies 'truncateDate()' with TimeUnit::month.
  TEST(TruncateDate, Month) {
      for (auto* timezone : kAllTimezones) {
          // Value in the middle of a bin above default reference point.
          ASSERT_EQ(timezone->createFromDateParts(2021, 3, 1, 0, 0, 0, 0),
                    truncateDate(timezone->createFromDateParts(2021, 4, 1, 0, 0, 0, 0),
                                 TimeUnit::month,
                                 2,
                                 *timezone));
  
          // Value at the lower boundary of a bin above default reference point.
          ASSERT_EQ(timezone->createFromDateParts(2021, 3, 1, 0, 0, 0, 0),
                    truncateDate(timezone->createFromDateParts(2021, 3, 1, 0, 0, 0, 0),
                                 TimeUnit::month,
                                 2,
                                 *timezone));
  
          // Value at the upper boundary of a bin above default reference point.
          ASSERT_EQ(timezone->createFromDateParts(2021, 3, 1, 0, 0, 0, 0),
                    truncateDate(timezone->createFromDateParts(2021, 4, 30, 23, 59, 59, 999),
                                 TimeUnit::month,
                                 2,
                                 *timezone));
  
          // Value at the lower boundary of the next bin.
          ASSERT_EQ(timezone->createFromDateParts(2021, 5, 1, 0, 0, 0, 0),
                    truncateDate(timezone->createFromDateParts(2021, 5, 1, 0, 0, 0, 0),
                                 TimeUnit::month,
                                 2,
                                 *timezone));
  
          // Value at the lower boundary of a bin.
          ASSERT_EQ(timezone->createFromDateParts(1900, 1, 1, 0, 0, 0, 0),
                    truncateDate(timezone->createFromDateParts(1900, 1, 1, 0, 0, 0, 0),
                                 TimeUnit::month,
                                 1,
                                 *timezone));
  
          // Value at the top of a bin.
          ASSERT_EQ(timezone->createFromDateParts(1900, 1, 1, 0, 0, 0, 0),
                    truncateDate(timezone->createFromDateParts(1900, 3, 31, 23, 59, 59, 999),
                                 TimeUnit::month,
                                 3,
                                 *timezone));
  
          // February is atypical.
          ASSERT_EQ(timezone->createFromDateParts(2005, 2, 1, 0, 0, 0, 0),
                    truncateDate(timezone->createFromDateParts(2005, 2, 28, 23, 59, 59, 999),
                                 TimeUnit::month,
                                 1,
                                 *timezone));
      }
  }
  
  // Verifies 'truncateDate()' with TimeUnit::quarter.
  TEST(TruncateDate, Quarter) {
      for (auto* timezone : kAllTimezones) {
          // Value in the middle of a bin above default reference point.
          ASSERT_EQ(timezone->createFromDateParts(2021, 1, 1, 0, 0, 0, 0),
                    truncateDate(timezone->createFromDateParts(2021, 5, 1, 0, 0, 0, 0),
                                 TimeUnit::quarter,
                                 2,
                                 *timezone));
  
          // Value at the lower boundary of a bin above default reference point.
          ASSERT_EQ(timezone->createFromDateParts(2021, 1, 1, 0, 0, 0, 0),
                    truncateDate(timezone->createFromDateParts(2021, 1, 1, 0, 0, 0, 0),
                                 TimeUnit::quarter,
                                 2,
                                 *timezone));
  
          // Value at the upper boundary of a bin above default reference point.
          ASSERT_EQ(timezone->createFromDateParts(2021, 1, 1, 0, 0, 0, 0),
                    truncateDate(timezone->createFromDateParts(2021, 6, 30, 23, 59, 59, 999),
                                 TimeUnit::quarter,
                                 2,
                                 *timezone));
  
          // Value at the lower boundary of the next bin.
          ASSERT_EQ(timezone->createFromDateParts(2021, 7, 1, 0, 0, 0, 0),
                    truncateDate(timezone->createFromDateParts(2021, 7, 1, 0, 0, 0, 0),
                                 TimeUnit::quarter,
                                 2,
                                 *timezone));
  
          // Value at the lower boundary of a bin.
          ASSERT_EQ(timezone->createFromDateParts(1900, 1, 1, 0, 0, 0, 0),
                    truncateDate(timezone->createFromDateParts(1900, 1, 1, 0, 0, 0, 0),
                                 TimeUnit::quarter,
                                 1,
                                 *timezone));
  
          // Value at the top of a bin.
          ASSERT_EQ(timezone->createFromDateParts(1900, 1, 1, 0, 0, 0, 0),
                    truncateDate(timezone->createFromDateParts(1900, 3, 31, 23, 59, 59, 999),
                                 TimeUnit::quarter,
                                 1,
                                 *timezone));
      }
  }
  
  // Verifies 'truncateDate()' with TimeUnit::year.
  TEST(TruncateDate, Year) {
      for (auto* timezone : kAllTimezones) {
          // Value in the middle of a bin above default reference point.
          ASSERT_EQ(timezone->createFromDateParts(2020, 1, 1, 0, 0, 0, 0),
                    truncateDate(timezone->createFromDateParts(2021, 5, 1, 0, 0, 0, 0),
                                 TimeUnit::year,
                                 2,
                                 *timezone));
  
          // Value at the lower boundary of a bin above default reference point.
          ASSERT_EQ(timezone->createFromDateParts(2020, 1, 1, 0, 0, 0, 0),
                    truncateDate(timezone->createFromDateParts(2020, 1, 1, 0, 0, 0, 0),
                                 TimeUnit::year,
                                 2,
                                 *timezone));
  
          // Value at the upper boundary of a bin above default reference point.
          ASSERT_EQ(timezone->createFromDateParts(2020, 1, 1, 0, 0, 0, 0),
                    truncateDate(timezone->createFromDateParts(2021, 12, 31, 23, 59, 59, 999),
                                 TimeUnit::year,
                                 2,
                                 *timezone));
  
          // Value at the lower boundary of the next bin.
          ASSERT_EQ(timezone->createFromDateParts(2022, 1, 1, 0, 0, 0, 0),
                    truncateDate(timezone->createFromDateParts(2022, 1, 1, 0, 0, 0, 0),
                                 TimeUnit::year,
                                 2,
                                 *timezone));
  
          // Value at the lower boundary of a bin.
          ASSERT_EQ(timezone->createFromDateParts(1950, 1, 1, 0, 0, 0, 0),
                    truncateDate(timezone->createFromDateParts(1950, 1, 1, 0, 0, 0, 0),
                                 TimeUnit::year,
                                 1,
                                 *timezone));
  
          // Value at the top of a bin.
          ASSERT_EQ(timezone->createFromDateParts(1950, 1, 1, 0, 0, 0, 0),
                    truncateDate(timezone->createFromDateParts(1950, 12, 31, 23, 59, 59, 999),
                                 TimeUnit::year,
                                 1,
                                 *timezone));
      }
  }
  
  // Verifies 'truncateDate()' error handling.
  TEST(TruncateDate, ErrorHandling) {
      auto* timezone = &kDefaultTimeZone;
      const auto anyDate = timezone->createFromDateParts(2000, 1, 1, 2, 0, 0, 0);
      const auto dateBeforeReferencePoint = timezone->createFromDateParts(1980, 1, 1, 0, 0, 0, 0);
  
      // Verify that binSize > 0 constraint is enforced.
      ASSERT_THROWS_CODE(
          truncateDate(anyDate, TimeUnit::day, 0, *timezone), AssertionException, 5439005);
  
      // Verify millisecond overflow detection.
      const Date_t dateInLongPast =
          Date_t::fromMillisSinceEpoch(std::numeric_limits<long long>::min());
      ASSERT_THROWS_CODE(truncateDate(dateInLongPast, TimeUnit::millisecond, 1, *timezone),
                         AssertionException,
                         5439000);
      ASSERT_THROWS_CODE(
          truncateDate(Date_t::fromMillisSinceEpoch(std::numeric_limits<long long>::min() / 4 * 3),
                       TimeUnit::millisecond,
                       std::numeric_limits<long long>::max() / 5 * 3,
                       *timezone),
          AssertionException,
          5439001);
  
      // Verify second/minute/hour overflow detection.
      for (auto timeUnit : {TimeUnit::second, TimeUnit::minute, TimeUnit::hour}) {
          ASSERT_THROWS_CODE(
              truncateDate(anyDate, timeUnit, std::numeric_limits<long long>::max() / 64, *timezone),
              AssertionException,
              5439002);
      }
  
      // Verify day/week/month/quarter/year bin size limits.
      for (auto timeUnit :
           {TimeUnit::day, TimeUnit::week, TimeUnit::month, TimeUnit::quarter, TimeUnit::year}) {
          ASSERT_THROWS_CODE(truncateDate(anyDate, timeUnit, 100'000'000'001ULL, *timezone),
                             AssertionException,
                             5439006);
  
          // Verify computation with large bin size when the result is in the long past succeeds.
          truncateDate(dateBeforeReferencePoint, timeUnit, 200'000'000ULL, *timezone);
  
          // Verify computation with large bin size succeeds.
          ASSERT_EQ(timezone->createFromDateParts(2000, 1, 1, 0, 0, 0, 0),
                    truncateDate(timezone->createFromDateParts(2000, 1, 1, 12, 0, 0, 0),
                                 timeUnit,
                                 100'000'000'000ULL,
                                 *timezone,
                                 DayOfWeek::saturday));
      }
  
      // Verify that out-of-range results are detected.
      ASSERT_THROWS_CODE(
          truncateDate(dateBeforeReferencePoint, TimeUnit::year, 1'000'000'000ULL, *timezone),
          AssertionException,
          5976500);
  
      // Verify computation with large bin size when the result is in the long past succeeds.
      ASSERT_EQ(timezone->createFromDateParts(-200'000'000LL, 1, 1, 0, 0, 0, 0),
                truncateDate(dateBeforeReferencePoint, TimeUnit::year, 200'002'000ULL, *timezone));
  }
  
  TEST(DateAdd, DateAddYear) {
      ASSERT_EQ(dateAdd(kDefaultTimeZone.createFromDateParts(2019, 11, 11, 0, 0, 0, 1),
                        TimeUnit::year,
                        1,
                        kDefaultTimeZone),
                kDefaultTimeZone.createFromDateParts(2020, 11, 11, 0, 0, 0, 1));
  
      ASSERT_EQ(dateAdd(kDefaultTimeZone.createFromDateParts(2019, 11, 11, 0, 0, 0, 0),
                        TimeUnit::year,
                        -1,
                        kDefaultTimeZone),
                kDefaultTimeZone.createFromDateParts(2018, 11, 11, 0, 0, 0, 0));
  
      ASSERT_EQ(dateAdd(kDefaultTimeZone.createFromDateParts(2016, 2, 29, 0, 0, 0, 0),
                        TimeUnit::year,
                        2,
                        kDefaultTimeZone),
                kDefaultTimeZone.createFromDateParts(2018, 2, 28, 0, 0, 0, 0));
  
      ASSERT_EQ(dateAdd(kDefaultTimeZone.createFromDateParts(2016, 2, 29, 0, 0, 0, 0),
                        TimeUnit::year,
                        -4,
                        kDefaultTimeZone),
                kDefaultTimeZone.createFromDateParts(2012, 2, 29, 0, 0, 0, 0));
  }
  
  TEST(DateAdd, LargeAmountValues) {
      const auto anyDate = kDefaultTimeZone.createFromDateParts(2016, 1, 1, 0, 0, 0, 0);
      const auto smallDate = kDefaultTimeZone.createFromDateParts(-291'000'000, 3, 31, 0, 0, 0, 0);
      struct TestCase {
          TimeUnit unit;
          long long invalidAmount;   // Amount value rejected in initial validation.
          long long overflowAmount;  // Amount to add to date -200'000'000-2-29 00:00:00.000 so the
                                     // result cannot be represented as Date_t.
          long long largeAmount;     // Large amount to add to 'smallDate' so the result is equal to
                                     // 'largeAmountExpectedDate'.
          Date_t largeAmountExpectedDate;
      };
      const auto maxValidYearAmountPlus1{584'942'417LL + 1};
      const auto maxValidDayAmountPlus1{213'503'982'334LL + 1};
      const std::vector<TestCase> testCases{
          {TimeUnit::year,
           maxValidYearAmountPlus1,      // Invalid amount.
           maxValidYearAmountPlus1 - 1,  // Overflow amount.
           550'000'000LL,                // Large amount.
           kDefaultTimeZone.createFromDateParts(-291'000'000 + 550'000'000, 3, 31, 0, 0, 0, 0)},
          {TimeUnit::quarter,
           maxValidYearAmountPlus1 * 4,      // Invalid amount.
           maxValidYearAmountPlus1 * 4 - 1,  // Overflow amount.
           550'000'000LL * 4,                // Large amount.
           kDefaultTimeZone.createFromDateParts(-291'000'000 + 550'000'000, 3, 31, 0, 0, 0, 0)},
          {TimeUnit::month,
           maxValidYearAmountPlus1 * 12,      // Invalid amount.
           maxValidYearAmountPlus1 * 12 - 1,  // Overflow amount.
           550'000'000LL * 12,                // Large amount.
           kDefaultTimeZone.createFromDateParts(-291'000'000 + 550'000'000, 3, 31, 0, 0, 0, 0)},
          {TimeUnit::day,
           maxValidDayAmountPlus1,      // Invalid amount.
           maxValidDayAmountPlus1 - 1,  // Overflow amount.
           250'000'000LL * 365,         // Large amount.
           smallDate + Days(250'000'000LL * 365)},
          {TimeUnit::hour,
           maxValidDayAmountPlus1 * 24,      // Invalid amount.
           maxValidDayAmountPlus1 * 24 - 1,  // Overflow amount.
           250'000'000LL * 365 * 24,         // Large amount.
           smallDate + Days(250'000'000LL * 365)},
          {TimeUnit::minute,
           maxValidDayAmountPlus1 * 24 * 60,      // Invalid amount.
           maxValidDayAmountPlus1 * 24 * 60 - 1,  // Overflow amount.
           250'000'000LL * 365 * 24 * 60,         // Large amount.
           smallDate + Days(250'000'000LL * 365)},
          {TimeUnit::second,
           maxValidDayAmountPlus1 * 24 * 60 * 60,      // Invalid amount.
           maxValidDayAmountPlus1 * 24 * 60 * 60 - 1,  // Overflow amount.
           250'000'000LL * 365 * 24 * 60 * 60,         // Large amount.
           smallDate + Days(250'000'000LL * 365)},
      };
      int testCaseIdx{0};
      for (auto&& testCase : testCases) {
          // Verify that out-of-range amount values are rejected.
          ASSERT_THROWS_CODE(
              dateAdd(anyDate, testCase.unit, testCase.invalidAmount, kDefaultTimeZone),
              AssertionException,
              5976500)
              << " test case# " << testCaseIdx;
          ASSERT_THROWS_CODE(
              dateAdd(anyDate, testCase.unit, -testCase.invalidAmount, kDefaultTimeZone),
              AssertionException,
              5976500)
              << " test case# " << testCaseIdx;
  
          // Verify that overflow is detected when the result cannot be represented as Date_t.
          ASSERT_THROWS_CODE(
              dateAdd(kDefaultTimeZone.createFromDateParts(-200'000'000, 2, 29, 0, 0, 0, 0),
                      testCase.unit,
                      testCase.overflowAmount,
                      kDefaultTimeZone),
              AssertionException,
              5166406)
              << " test case# " << testCaseIdx;
  
          // Verify that adding large values works correctly.
          ASSERT_EQ(dateAdd(smallDate, testCase.unit, testCase.largeAmount, kDefaultTimeZone),
                    testCase.largeAmountExpectedDate)
              << " test case# " << testCaseIdx;
  
          ++testCaseIdx;
      }
  }
  
  TEST(DateAdd, DateAddQuarter) {
      auto startDate = kDefaultTimeZone.createFromDateParts(2020, 1, 1, 0, 0, 0, 3);
      ASSERT_EQ(dateAdd(startDate, TimeUnit::quarter, 1, kDefaultTimeZone),
                kDefaultTimeZone.createFromDateParts(2020, 4, 1, 0, 0, 0, 3));
  
      ASSERT_EQ(dateAdd(startDate, TimeUnit::quarter, -5, kDefaultTimeZone),
                kDefaultTimeZone.createFromDateParts(2018, 10, 1, 0, 0, 0, 3));
  
      ASSERT_EQ(dateAdd(kDefaultTimeZone.createFromDateParts(2020, 1, 31, 0, 0, 0, 0),
                        TimeUnit::quarter,
                        1,
                        kDefaultTimeZone),
                kDefaultTimeZone.createFromDateParts(2020, 4, 30, 0, 0, 0, 0));
  
      ASSERT_EQ(dateAdd(kDefaultTimeZone.createFromDateParts(2020, 1, 31, 0, 0, 0, 0),
                        TimeUnit::quarter,
                        -3,
                        kDefaultTimeZone),
                kDefaultTimeZone.createFromDateParts(2019, 4, 30, 0, 0, 0, 0));
  }
  
  TEST(DateAdd, DateAddMonth) {
      auto utc2020_08_31 = kDefaultTimeZone.createFromDateParts(2020, 8, 31, 10, 5, 0, 5);
      auto utc2021_04_30 = kDefaultTimeZone.createFromDateParts(2021, 4, 30, 0, 0, 0, 0);
      auto utc2021_03_01 = kDefaultTimeZone.createFromDateParts(2021, 3, 1, 0, 0, 0, 0);
  
      ASSERT_EQ(dateAdd(utc2020_08_31, TimeUnit::month, 1, kDefaultTimeZone),
                kDefaultTimeZone.createFromDateParts(2020, 9, 30, 10, 5, 0, 5));
  
      ASSERT_EQ(dateAdd(utc2020_08_31, TimeUnit::month, 5, kDefaultTimeZone),
                kDefaultTimeZone.createFromDateParts(2021, 1, 31, 10, 5, 0, 5));
  
      ASSERT_EQ(dateAdd(utc2020_08_31, TimeUnit::month, 6, kDefaultTimeZone),
                kDefaultTimeZone.createFromDateParts(2021, 2, 28, 10, 5, 0, 5));
  
      ASSERT_EQ(dateAdd(utc2020_08_31, TimeUnit::month, -1, kDefaultTimeZone),
                kDefaultTimeZone.createFromDateParts(2020, 7, 31, 10, 5, 0, 5));
  
      ASSERT_EQ(dateAdd(utc2020_08_31, TimeUnit::month, -4, kDefaultTimeZone),
                kDefaultTimeZone.createFromDateParts(2020, 4, 30, 10, 5, 0, 5));
  
      ASSERT_EQ(dateAdd(utc2020_08_31, TimeUnit::month, -6, kDefaultTimeZone),
                kDefaultTimeZone.createFromDateParts(2020, 2, 29, 10, 5, 0, 5));
  
      ASSERT_EQ(dateAdd(utc2020_08_31, TimeUnit::month, -18, kDefaultTimeZone),
                kDefaultTimeZone.createFromDateParts(2019, 2, 28, 10, 5, 0, 5));
  
      ASSERT_EQ(dateAdd(utc2021_04_30, TimeUnit::month, 1, kDefaultTimeZone),
                kDefaultTimeZone.createFromDateParts(2021, 5, 30, 0, 0, 0, 0));
  
      ASSERT_EQ(dateAdd(utc2021_04_30, TimeUnit::month, -2, kDefaultTimeZone),
                kDefaultTimeZone.createFromDateParts(2021, 2, 28, 0, 0, 0, 0));
  
      ASSERT_EQ(dateAdd(utc2021_03_01, TimeUnit::month, 2, kDefaultTimeZone),
                kDefaultTimeZone.createFromDateParts(2021, 5, 1, 0, 0, 0, 0));
  
      ASSERT_EQ(dateAdd(utc2021_03_01, TimeUnit::month, -1, kDefaultTimeZone),
                kDefaultTimeZone.createFromDateParts(2021, 2, 1, 0, 0, 0, 0));
  }
  
  /**
   * Tests for day adjustment in a timezone with units of month, quarter or year.
   */
  TEST(DateAdd, DateAddDayAdjustmentWithTimezone) {
      struct TestCase {
          Date_t startDate;
          TimeUnit unit;
          long long amount;
          Date_t expectedDate;
      };
  
      auto europeAmsterdamZone = kDefaultTimeZoneDatabase.getTimeZone("Europe/Amsterdam");
      // Last day of month in Amsterdam zone 2021-05-31T00:30:00.
      auto ams2021_05_31T00_30 = europeAmsterdamZone.createFromDateParts(2021, 05, 31, 0, 30, 0, 4);
      // First day in Amsterdam zone, last day in UTC.
      auto ams2021_02_01 = europeAmsterdamZone.createFromDateParts(2021, 2, 1, 0, 0, 0, 0);
  
      const std::vector<TestCase> testsAmsterdamZone{
          {ams2021_05_31T00_30,
           TimeUnit::month,
           1,
           europeAmsterdamZone.createFromDateParts(2021, 06, 30, 0, 30, 0, 4)},
          {ams2021_05_31T00_30,
           TimeUnit::month,
           -1,
           europeAmsterdamZone.createFromDateParts(2021, 04, 30, 0, 30, 0, 4)},
          {ams2021_05_31T00_30,
           TimeUnit::quarter,
           -1,
           europeAmsterdamZone.createFromDateParts(2021, 02, 28, 0, 30, 0, 4)},
          {ams2021_05_31T00_30,
           TimeUnit::quarter,
           -5,
           europeAmsterdamZone.createFromDateParts(2020, 02, 29, 0, 30, 0, 4)},
          {ams2021_02_01,
           TimeUnit::month,
           1,
           europeAmsterdamZone.createFromDateParts(2021, 3, 1, 0, 0, 0, 0)},
          {ams2021_02_01,
           TimeUnit::month,
           -2,
           europeAmsterdamZone.createFromDateParts(2020, 12, 1, 0, 0, 0, 0)},
          {ams2021_02_01,
           TimeUnit::quarter,
           1,
           europeAmsterdamZone.createFromDateParts(2021, 5, 1, 0, 0, 0, 0)},
      };
      for (const auto& test : testsAmsterdamZone) {
          const auto testNumber = &test - &testsAmsterdamZone.front();
          ASSERT_EQ(dateAdd(test.startDate, test.unit, test.amount, europeAmsterdamZone),
                    test.expectedDate)
              << "on test " << testNumber;
      }
  
      auto europeSofiaZone = kDefaultTimeZoneDatabase.getTimeZone("Europe/Sofia");
      // First day in Sofia timezone, last day of month in UTC.
      auto sofia2021_02_01 = europeSofiaZone.createFromDateParts(2021, 2, 1, 0, 0, 0, 5);
      auto sofia2021_03_01 = europeSofiaZone.createFromDateParts(2021, 3, 1, 0, 0, 0, 0);
  
      const std::vector<TestCase> testsSofiaZone{
          {sofia2021_02_01,
           TimeUnit::month,
           1,
           europeSofiaZone.createFromDateParts(2021, 3, 1, 0, 0, 0, 5)},
          {sofia2021_02_01,
           TimeUnit::month,
           -2,
           europeSofiaZone.createFromDateParts(2020, 12, 1, 0, 0, 0, 5)},
          {sofia2021_02_01,
           TimeUnit::quarter,
           1,
           europeSofiaZone.createFromDateParts(2021, 5, 1, 0, 0, 0, 5)},
          {sofia2021_03_01,
           TimeUnit::month,
           1,
           europeSofiaZone.createFromDateParts(2021, 4, 1, 0, 0, 0, 0)},
          {sofia2021_03_01,
           TimeUnit::month,
           -1,
           europeSofiaZone.createFromDateParts(2021, 2, 1, 0, 0, 0, 0)},
          {sofia2021_03_01,
           TimeUnit::quarter,
           1,
           europeSofiaZone.createFromDateParts(2021, 6, 1, 0, 0, 0, 0)}};
  
      for (const auto& test : testsSofiaZone) {
          const auto testNumber = &test - &testsSofiaZone.front();
          ASSERT_EQ(dateAdd(test.startDate, test.unit, test.amount, europeSofiaZone),
                    test.expectedDate)
              << "on test " << testNumber;
      }
  
      auto newYorkZone = kDefaultTimeZoneDatabase.getTimeZone("America/New_York");
      auto ny2020_12_30T22 = kDefaultTimeZone.createFromDateParts(2020, 12, 31, 3, 0, 0, 0);
      auto ny2021_04_30T20 = newYorkZone.createFromDateParts(2021, 4, 30, 20, 0, 0, 6);
      auto ny2021_03_01 = newYorkZone.createFromDateParts(2021, 3, 1, 0, 0, 0, 0);
  
      const std::vector<TestCase> testsNYZone{
          {ny2020_12_30T22,
           TimeUnit::month,
           -1,
           kDefaultTimeZone.createFromDateParts(
               2020, 12, 1, 3, 0, 0, 0)},  // 2020-11-30T22:00:00 NY zone
          {ny2020_12_30T22,
           TimeUnit::month,
           2,
           kDefaultTimeZone.createFromDateParts(
               2021, 3, 1, 3, 0, 0, 0)},  // 2021-02-28T22:00:00 NY zone
          {ny2020_12_30T22,
           TimeUnit::quarter,
           -2,
           kDefaultTimeZone.createFromDateParts(
               2020, 7, 1, 2, 0, 0, 0)},  // 2020-06-30T22:00:00 NY zone
          {ny2020_12_30T22,
           TimeUnit::month,
           2,
           kDefaultTimeZone.createFromDateParts(
               2021, 3, 1, 3, 0, 0, 0)},  // 2021-02-28T22:00:00 NY zone
          {ny2021_04_30T20,
           TimeUnit::month,
           1,
           newYorkZone.createFromDateParts(2021, 5, 30, 20, 0, 0, 6)},
          {ny2021_04_30T20,
           TimeUnit::month,
           -2,
           newYorkZone.createFromDateParts(2021, 2, 28, 20, 0, 0, 6)},
          {ny2021_03_01, TimeUnit::month, 1, newYorkZone.createFromDateParts(2021, 4, 1, 0, 0, 0, 0)},
          {ny2021_03_01,
           TimeUnit::month,
           -1,
           newYorkZone.createFromDateParts(2021, 2, 1, 0, 0, 0, 0)},
          {ny2021_03_01,
           TimeUnit::quarter,
           1,
           newYorkZone.createFromDateParts(2021, 6, 1, 0, 0, 0, 0)},
      };
  
      for (const auto& test : testsNYZone) {
          const auto testNumber = &test - &testsNYZone.front();
          ASSERT_EQ(dateAdd(test.startDate, test.unit, test.amount, newYorkZone), test.expectedDate)
              << "on test " << testNumber;
      }
  
      auto australiaEuclaZone = kDefaultTimeZoneDatabase.getTimeZone("Australia/Eucla");
      auto eucla2021_02_01 = australiaEuclaZone.createFromDateParts(2021, 2, 1, 0, 0, 0, 7);
      auto eucla2021_04_30 = australiaEuclaZone.createFromDateParts(2021, 4, 30, 0, 0, 0, 0);
      const std::vector<TestCase> testsEuclaZone{
          {eucla2021_02_01,
           TimeUnit::month,
           1,
           australiaEuclaZone.createFromDateParts(2021, 3, 1, 0, 0, 0, 7)},
          {eucla2021_02_01,
           TimeUnit::month,
           -2,
           australiaEuclaZone.createFromDateParts(2020, 12, 1, 0, 0, 0, 7)},
          {eucla2021_02_01,
           TimeUnit::quarter,
           1,
           australiaEuclaZone.createFromDateParts(2021, 5, 1, 0, 0, 0, 7)},
          {eucla2021_04_30,
           TimeUnit::month,
           1,
           australiaEuclaZone.createFromDateParts(2021, 5, 30, 0, 0, 0, 0)},
          {eucla2021_04_30,
           TimeUnit::month,
           -2,
           australiaEuclaZone.createFromDateParts(2021, 2, 28, 0, 0, 0, 0)},
          {eucla2021_04_30,
           TimeUnit::quarter,
           1,
           australiaEuclaZone.createFromDateParts(2021, 7, 30, 0, 0, 0, 0)}};
  
      for (const auto& test : testsEuclaZone) {
          const auto testNumber = &test - &testsEuclaZone.front();
          ASSERT_EQ(dateAdd(test.startDate, test.unit, test.amount, australiaEuclaZone),
                    test.expectedDate)
              << "on test " << testNumber;
      }
  }
  
  TEST(DateAdd, DateAddDay) {
      auto startDate = kDefaultTimeZone.createFromDateParts(2020, 8, 31, 10, 5, 0, 7);
      ASSERT_EQ(dateAdd(startDate, TimeUnit::day, 1, kDefaultTimeZone),
                kDefaultTimeZone.createFromDateParts(2020, 9, 1, 10, 5, 0, 7));
  
      ASSERT_EQ(dateAdd(startDate, TimeUnit::day, -1, kDefaultTimeZone),
                kDefaultTimeZone.createFromDateParts(2020, 8, 30, 10, 5, 0, 7));
  
      ASSERT_EQ(dateAdd(startDate, TimeUnit::day, -366, kDefaultTimeZone),
                kDefaultTimeZone.createFromDateParts(2019, 8, 31, 10, 5, 0, 7));
  }
  
  TEST(DateAdd, DateAddHour) {
      struct TestCase {
          Date_t startDate;
          long long amount;
          Date_t expectedDate;
      };
  
      auto utcStartDate = kDefaultTimeZone.createFromDateParts(2020, 8, 31, 10, 5, 0, 9);
  
      std::vector<TestCase> tests{
          {utcStartDate, 1, kDefaultTimeZone.createFromDateParts(2020, 8, 31, 11, 5, 0, 9)},
          {utcStartDate, -1, kDefaultTimeZone.createFromDateParts(2020, 8, 31, 9, 5, 0, 9)},
          {utcStartDate, 168, kDefaultTimeZone.createFromDateParts(2020, 9, 7, 10, 5, 0, 9)},
      };
  
      for (auto&& test : tests) {
          ASSERT_EQ(dateAdd(test.startDate, TimeUnit::hour, test.amount, kDefaultTimeZone),
                    test.expectedDate);
      }
  }
  
  TEST(DateAdd, DateAddMinute) {
      auto startDate = kDefaultTimeZone.createFromDateParts(2020, 12, 31, 23, 55, 15, 8);
      ASSERT_EQ(dateAdd(startDate, TimeUnit::minute, 1, kDefaultTimeZone),
                kDefaultTimeZone.createFromDateParts(2020, 12, 31, 23, 56, 15, 8));
  
      ASSERT_EQ(dateAdd(startDate, TimeUnit::minute, -1, kDefaultTimeZone),
                kDefaultTimeZone.createFromDateParts(2020, 12, 31, 23, 54, 15, 8));
  
      ASSERT_EQ(dateAdd(startDate, TimeUnit::minute, 10, kDefaultTimeZone),
                kDefaultTimeZone.createFromDateParts(2021, 1, 1, 0, 5, 15, 8));
  
      ASSERT_EQ(dateAdd(startDate, TimeUnit::minute, -1440, kDefaultTimeZone),
                kDefaultTimeZone.createFromDateParts(2020, 12, 30, 23, 55, 15, 8));
  }
  
  /**
   * Tests for dateAdd operation in a timezone different from UTC.
   */
  TEST(DateAdd, DateAddWithTimezoneDST) {
      struct TestCase {
          Date_t startDate;
          TimeUnit unit;
          long long amount;
          Date_t expectedDate;
      };
  
      auto europeAmsterdamZone = kDefaultTimeZoneDatabase.getTimeZone("Europe/Amsterdam");
      // The test uses the DST change in Amsterdam zone that happens at 2020-10-25T01:00:00 UTC.
      // Pick two time instants in UTC just before and after the DST change.
      auto startDateBeforeDSTChange = kDefaultTimeZone.createFromDateParts(2020, 10, 25, 0, 50, 5, 0);
      auto startDateAfterDSTChange = kDefaultTimeZone.createFromDateParts(2020, 10, 25, 1, 10, 5, 0);
  
      std::vector<TestCase> tests{
          // DST to Standard change in Amsterdam zone that happens at 2020-10-25T01:00:00 UTC.
          // 2020-10-25T03:00:00 -> 2020-10-25T02:00:00 in Amsterdam.
          {startDateBeforeDSTChange,
           TimeUnit::minute,
           10,
           kDefaultTimeZone.createFromDateParts(2020, 10, 25, 1, 0, 5, 0)},
          {startDateBeforeDSTChange,
           TimeUnit::hour,
           1,
           kDefaultTimeZone.createFromDateParts(2020, 10, 25, 1, 50, 5, 0)},
          {startDateAfterDSTChange,
           TimeUnit::minute,
           -20,
           kDefaultTimeZone.createFromDateParts(2020, 10, 25, 0, 50, 5, 0)},
          {startDateAfterDSTChange,
           TimeUnit::hour,
           -1,
           kDefaultTimeZone.createFromDateParts(2020, 10, 25, 0, 10, 5, 0)},
  
          // For units of day or larger, compute DST correction to produce the same local time in
          // Amsterdam timezone.
          {europeAmsterdamZone.createFromDateParts(2020, 10, 25, 1, 50, 0, 0),
           TimeUnit::day,
           1,
           europeAmsterdamZone.createFromDateParts(2020, 10, 26, 1, 50, 0, 0)},
          {europeAmsterdamZone.createFromDateParts(2020, 10, 25, 2, 30, 0, 0),
           TimeUnit::day,
           1,
           europeAmsterdamZone.createFromDateParts(2020, 10, 26, 2, 30, 0, 0)},
          {europeAmsterdamZone.createFromDateParts(2020, 10, 24, 2, 0, 1, 0),
           TimeUnit::day,
           1,
           europeAmsterdamZone.createFromDateParts(2020, 10, 25, 2, 0, 1, 0)},
          {europeAmsterdamZone.createFromDateParts(2020, 10, 24, 3, 0, 1, 0),
           TimeUnit::day,
           1,
           kDefaultTimeZone.createFromDateParts(2020, 10, 25, 2, 0, 1, 0)},
          {europeAmsterdamZone.createFromDateParts(2020, 10, 24, 3, 0, 1, 0),
           TimeUnit::day,
           1,
           // The same as above in Amsterdam zone.
           europeAmsterdamZone.createFromDateParts(2020, 10, 25, 3, 0, 1, 0)},
          {europeAmsterdamZone.createFromDateParts(2020, 10, 25, 3, 30, 0, 0),
           TimeUnit::day,
           -1,
           europeAmsterdamZone.createFromDateParts(2020, 10, 24, 3, 30, 0, 0)},
          {europeAmsterdamZone.createFromDateParts(2020, 10, 25, 2, 30, 0, 0),
           TimeUnit::day,
           -1,
           europeAmsterdamZone.createFromDateParts(2020, 10, 24, 2, 30, 0, 0)},
          {europeAmsterdamZone.createFromDateParts(2020, 10, 26, 2, 30, 0, 0),
           TimeUnit::day,
           -1,
           europeAmsterdamZone.createFromDateParts(2020, 10, 25, 2, 30, 0, 0)},
  
          // Standard to DST change at 2021-03-28T02:00:00 -> 2021-03-28T03:00:00 Amsterdam timezone.
          {europeAmsterdamZone.createFromDateParts(2021, 3, 28, 1, 20, 0, 0),
           TimeUnit::day,
           1,
           europeAmsterdamZone.createFromDateParts(2021, 3, 29, 1, 20, 0, 0)},
          {europeAmsterdamZone.createFromDateParts(2021, 3, 27, 2, 20, 0, 0),
           TimeUnit::day,
           1,
           // Computed time falls into the missing hour: forward the clock 1 hour.
           europeAmsterdamZone.createFromDateParts(2021, 3, 28, 3, 20, 0, 0)},
          {europeAmsterdamZone.createFromDateParts(2021, 3, 28, 3, 20, 0, 0),
           TimeUnit::day,
           -1,
           europeAmsterdamZone.createFromDateParts(2021, 3, 27, 3, 20, 0, 0)},
          {europeAmsterdamZone.createFromDateParts(2021, 3, 29, 2, 20, 0, 0),
           TimeUnit::day,
           -1,
           // Computed time falls into the missing hour: forward the clock 1 hour.
           europeAmsterdamZone.createFromDateParts(2021, 3, 28, 3, 20, 0, 0)},
          {europeAmsterdamZone.createFromDateParts(2020, 11, 28, 2, 20, 0, 0),
           TimeUnit::month,
           4,
           // Computed time falls into the missing hour: forward the clock 1 hour.
           europeAmsterdamZone.createFromDateParts(2021, 3, 28, 3, 20, 0, 0)},
      };
  
      for (auto&& test : tests) {
          const auto testNumber = &test - &tests.front();
          ASSERT_EQ(dateAdd(test.startDate, test.unit, test.amount, europeAmsterdamZone),
                    test.expectedDate)
              << " on test " << testNumber << " in Amsterdam timezone.";
      }
  
      auto newYorkZone = kDefaultTimeZoneDatabase.getTimeZone("America/New_York");
      // The following test uses the DST change in New York at 2020-03-08T07:00:00Z.
      // Pick two time instants in UTC just before and after the DST change.
      auto startDateInEST = kDefaultTimeZone.createFromDateParts(2020, 3, 8, 6, 50, 0, 0);
      auto startDateInEDT = kDefaultTimeZone.createFromDateParts(2020, 3, 8, 7, 20, 0, 0);
  
      std::vector<TestCase> testsNYZone{
          {startDateInEST,
           TimeUnit::minute,
           15,
           kDefaultTimeZone.createFromDateParts(2020, 3, 8, 7, 5, 0, 0)},
          {startDateInEST,
           TimeUnit::hour,
           1,
           kDefaultTimeZone.createFromDateParts(2020, 3, 8, 7, 50, 0, 0)},
          {startDateInEDT,
           TimeUnit::minute,
           -25,
           kDefaultTimeZone.createFromDateParts(2020, 3, 8, 6, 55, 0, 0)},
          {startDateInEDT,
           TimeUnit::hour,
           -1,
           kDefaultTimeZone.createFromDateParts(2020, 3, 8, 6, 20, 0, 0)},
          // For units of day or larger, compute DST correction to produce the same local time in New
          // York timezone.
  
          // DST to Standard change in New York zone at 2020-11-01T06:00:00 UTC,
          // 2020-11-01T02:00:00 -> 2020-11-01T01:00:00 New York.
          {newYorkZone.createFromDateParts(2020, 11, 1, 0, 30, 0, 0),
           TimeUnit::day,
           1,
           newYorkZone.createFromDateParts(2020, 11, 2, 0, 30, 0, 0)},
          {newYorkZone.createFromDateParts(2020, 11, 1, 1, 30, 0, 0),
           TimeUnit::day,
           1,
           newYorkZone.createFromDateParts(2020, 11, 2, 1, 30, 0, 0)},
          {newYorkZone.createFromDateParts(2020, 10, 31, 1, 30, 0, 0),
           TimeUnit::day,
           1,
           newYorkZone.createFromDateParts(2020, 11, 1, 1, 30, 0, 0)},
          {newYorkZone.createFromDateParts(2020, 11, 1, 3, 0, 0, 0),
           TimeUnit::day,
           -1,
           newYorkZone.createFromDateParts(2020, 10, 31, 3, 0, 0, 0)},
          {newYorkZone.createFromDateParts(2020, 11, 1, 1, 30, 0, 0),
           TimeUnit::day,
           -1,
           newYorkZone.createFromDateParts(2020, 10, 31, 1, 30, 0, 0)},
          {newYorkZone.createFromDateParts(2020, 11, 2, 1, 30, 0, 0),
           TimeUnit::day,
           -1,
           newYorkZone.createFromDateParts(2020, 11, 1, 1, 30, 0, 0)},
  
          // Standard to DST change: 2021-03-14T02:00:00 -> 2021-03-14T03:00:00 New York timezone.
          {newYorkZone.createFromDateParts(2021, 3, 14, 1, 30, 0, 0),
           TimeUnit::day,
           1,
           newYorkZone.createFromDateParts(2021, 3, 15, 1, 30, 0, 0)},
          {newYorkZone.createFromDateParts(2021, 3, 13, 2, 20, 0, 0),
           TimeUnit::day,
           1,
           // Computed time falls into the missing hour: forward the clock 1 hour.
           newYorkZone.createFromDateParts(2021, 3, 14, 3, 20, 0, 0)},
          {newYorkZone.createFromDateParts(2021, 3, 14, 3, 20, 0, 0),
           TimeUnit::day,
           -1,
           newYorkZone.createFromDateParts(2021, 3, 13, 3, 20, 0, 0)},
          {newYorkZone.createFromDateParts(2021, 3, 15, 2, 20, 0, 0),
           TimeUnit::day,
           -1,
           // Computed time falls into the missing hour: forward the clock 1 hour.
           newYorkZone.createFromDateParts(2021, 3, 14, 3, 20, 0, 0)},
          {newYorkZone.createFromDateParts(2021, 1, 14, 2, 20, 0, 0),
           TimeUnit::month,
           2,
           // Computed time falls into the missing hour: forward the clock 1 hour.
           newYorkZone.createFromDateParts(2021, 3, 14, 3, 20, 0, 0)},
      };
  
      for (auto&& test : testsNYZone) {
          const auto testNumber = &test - &testsNYZone.front();
          ASSERT_EQ(dateAdd(test.startDate, test.unit, test.amount, newYorkZone), test.expectedDate)
              << " on test " << testNumber << " in New York timezone.";
      }
  }
  
  /**
   * Tests for dateAdd operation in Lord Howe timezone with 00:30 DST change.
   */
  TEST(DateAdd, DateAdd_LordHoweTimezoneDST) {
      struct TestCase {
          Date_t startDate;
          TimeUnit unit;
          long long amount;
          Date_t expectedDate;
      };
  
      auto australiaLordHoweZone = kDefaultTimeZoneDatabase.getTimeZone("Australia/Lord_Howe");
      std::vector<TestCase> tests{
          // DST to Standard change: 2021-04-04T02:00:00 -> 2021-04-04T01:30:00 Lord Howe timezone.
          {australiaLordHoweZone.createFromDateParts(2021, 4, 4, 1, 30, 0, 0),
           TimeUnit::day,
           1,
           australiaLordHoweZone.createFromDateParts(2021, 4, 5, 1, 30, 0, 0)},
          {australiaLordHoweZone.createFromDateParts(2021, 4, 3, 1, 45, 0, 0),
           TimeUnit::day,
           1,
           // Computed time falls into the repeated 1/2 hour.
           australiaLordHoweZone.createFromDateParts(2021, 4, 4, 1, 45, 0, 0)},
          {australiaLordHoweZone.createFromDateParts(2021, 4, 5, 1, 0, 0, 0),
           TimeUnit::day,
           -1,
           australiaLordHoweZone.createFromDateParts(2021, 4, 4, 1, 0, 0, 0)},
          {australiaLordHoweZone.createFromDateParts(2021, 3, 4, 5, 30, 0, 5),
           TimeUnit::month,
           1,
           australiaLordHoweZone.createFromDateParts(2021, 4, 4, 5, 30, 0, 5)},
  
          // DST to Standard change: 2021-04-03T15:00:00 UTC time.
          {kDefaultTimeZone.createFromDateParts(2021, 4, 3, 14, 50, 5, 0),
           TimeUnit::minute,
           20,
           kDefaultTimeZone.createFromDateParts(2021, 4, 3, 15, 10, 5, 0)},
          {kDefaultTimeZone.createFromDateParts(2021, 4, 3, 14, 50, 5, 0),
           // = 2021-04-04T01:50:00 Australia/Lord_Howe, DST offset +11:00.
           TimeUnit::minute,
           20,
           // Same as above, the result date falls into repeated 1/2 hour, reported with standard time
           // offset of +10:30.
           australiaLordHoweZone.createFromDateParts(2021, 4, 4, 1, 40, 5, 0)},
          {kDefaultTimeZone.createFromDateParts(2021, 4, 3, 14, 50, 5, 0),
           TimeUnit::hour,
           1,
           kDefaultTimeZone.createFromDateParts(2021, 4, 3, 15, 50, 5, 0)},
          {kDefaultTimeZone.createFromDateParts(2021, 4, 3, 14, 50, 5, 0),
           // = 2021-04-04T01:50:00 Australia/Lord_Howe.
           TimeUnit::hour,
           1,
           // Same as above, the result local time has offset of +10:30 - Standard time.
           australiaLordHoweZone.createFromDateParts(2021, 4, 4, 2, 20, 5, 0)},
  
          // Standard to DST change: 2020-10-04T02:00:00 -> 2020-10-04T02:30:00 Lord Howe timezone.
          {australiaLordHoweZone.createFromDateParts(2020, 10, 4, 1, 30, 0, 0),
           TimeUnit::day,
           1,
           australiaLordHoweZone.createFromDateParts(2020, 10, 5, 1, 30, 0, 0)},
          {australiaLordHoweZone.createFromDateParts(2020, 10, 3, 2, 15, 0, 0),
           TimeUnit::day,
           1,
           // Computed time falls into the missing 1/2 hour: move the clock forward by 30 min.
           australiaLordHoweZone.createFromDateParts(2020, 10, 4, 2, 45, 0, 0)},
          {australiaLordHoweZone.createFromDateParts(2020, 10, 5, 1, 0, 0, 8),
           TimeUnit::day,
           -1,
           australiaLordHoweZone.createFromDateParts(2020, 10, 4, 1, 0, 0, 8)},
          {australiaLordHoweZone.createFromDateParts(2021, 1, 3, 18, 25, 0, 0),
           TimeUnit::month,
           -3,
           australiaLordHoweZone.createFromDateParts(2020, 10, 3, 18, 25, 0, 0)},
  
          // Standard to DST change: 2020-10-03T15:30:00 UTC time.
          {kDefaultTimeZone.createFromDateParts(2020, 10, 3, 15, 20, 5, 0),
           TimeUnit::minute,
           20,
           kDefaultTimeZone.createFromDateParts(2020, 10, 3, 15, 40, 5, 0)},
          {kDefaultTimeZone.createFromDateParts(2020, 10, 3, 15, 20, 5, 0),
           TimeUnit::hour,
           1,
           kDefaultTimeZone.createFromDateParts(2020, 10, 3, 16, 20, 5, 0)},
          {kDefaultTimeZone.createFromDateParts(2020, 10, 3, 15, 30, 20, 0),
           TimeUnit::second,
           -50,
           kDefaultTimeZone.createFromDateParts(2020, 10, 3, 15, 29, 30, 0)},
      };
  
      for (auto&& test : tests) {
          const auto testNumber = &test - &tests.front();
          ASSERT_EQ(dateAdd(test.startDate, test.unit, test.amount, australiaLordHoweZone),
                    test.expectedDate)
              << " on test " << testNumber << " in Australia/Lord_Howe timezone.";
      }
  }
  
  /**
   * Tests for dateAdd operation in a timezone with fixed offset.
   */
  TEST(DateAdd, DateAdd_offsetTimezone) {
      struct TestCase {
          Date_t startDate;
          TimeUnit unit;
          long long amount;
          Date_t expectedDate;
      };
  
      auto plus2TimeZone = kDefaultTimeZoneDatabase.getTimeZone("+02:00");
      std::vector<TestCase> tests{
          {plus2TimeZone.createFromDateParts(2021, 3, 1, 1, 30, 0, 0),
           TimeUnit::month,
           1,
           plus2TimeZone.createFromDateParts(2021, 4, 1, 1, 30, 0, 0)},
          {plus2TimeZone.createFromDateParts(2021, 3, 31, 1, 30, 0, 0),
           TimeUnit::month,
           1,
           plus2TimeZone.createFromDateParts(2021, 4, 30, 1, 30, 0, 0)},
          {plus2TimeZone.createFromDateParts(2021, 3, 28, 1, 30, 0, 0),
           TimeUnit::day,
           1,
           plus2TimeZone.createFromDateParts(2021, 3, 29, 1, 30, 0, 0)},
          {plus2TimeZone.createFromDateParts(2021, 3, 28, 1, 30, 0, 0),
           TimeUnit::hour,
           1,
           plus2TimeZone.createFromDateParts(2021, 3, 28, 2, 30, 0, 0)},
      };
  
      for (auto&& test : tests) {
          const auto testNumber = &test - &tests.front();
          ASSERT_EQ(dateAdd(test.startDate, test.unit, test.amount, plus2TimeZone), test.expectedDate)
              << " on test " << testNumber << " in timezone +02:00.";
      }
  }
  
  TEST(DateAdd, DateAddSecond) {
      auto startDate = kDefaultTimeZone.createFromDateParts(2020, 12, 31, 23, 55, 15, 2);
      ASSERT_EQ(dateAdd(startDate, TimeUnit::second, 1, kDefaultTimeZone),
                kDefaultTimeZone.createFromDateParts(2020, 12, 31, 23, 55, 16, 2));
  
      ASSERT_EQ(dateAdd(startDate, TimeUnit::second, -1, kDefaultTimeZone),
                kDefaultTimeZone.createFromDateParts(2020, 12, 31, 23, 55, 14, 2));
  
      ASSERT_EQ(dateAdd(startDate, TimeUnit::second, 300, kDefaultTimeZone),
                kDefaultTimeZone.createFromDateParts(2021, 1, 1, 0, 0, 15, 2));
  
      ASSERT_EQ(dateAdd(startDate, TimeUnit::second, -195, kDefaultTimeZone),
                kDefaultTimeZone.createFromDateParts(2020, 12, 31, 23, 52, 0, 2));
  }
  
  TEST(DateAdd, DateAddMillisecond) {
      auto startDate = kDefaultTimeZone.createFromDateParts(2020, 12, 31, 23, 59, 15, 1);
      ASSERT_EQ(dateAdd(startDate, TimeUnit::millisecond, 1, kDefaultTimeZone),
                kDefaultTimeZone.createFromDateParts(2020, 12, 31, 23, 59, 15, 2));
  
      ASSERT_EQ(dateAdd(startDate, TimeUnit::millisecond, -2, kDefaultTimeZone),
                kDefaultTimeZone.createFromDateParts(2020, 12, 31, 23, 59, 14, 999));
  
      ASSERT_EQ(dateAdd(startDate, TimeUnit::millisecond, 45000, kDefaultTimeZone),
                kDefaultTimeZone.createFromDateParts(2021, 1, 1, 0, 0, 0, 1));
  
      ASSERT_EQ(dateAdd(startDate, TimeUnit::millisecond, -1501, kDefaultTimeZone),
                kDefaultTimeZone.createFromDateParts(2020, 12, 31, 23, 59, 13, 500));
  
      // Verify that an overflow is detected.
      ASSERT_THROWS_CODE(dateAdd(startDate,
                                 TimeUnit::millisecond,
                                 std::numeric_limits<long long>::max(),
                                 kDefaultTimeZone),
                         AssertionException,
                         ErrorCodes::Error::DurationOverflow);
  }
  
  TEST(IsValidDayOfWeek, Basic) {
      ASSERT(isValidDayOfWeek("monday"));
      ASSERT(isValidDayOfWeek("tue"));
      ASSERT(isValidDayOfWeek("Wednesday"));
      ASSERT(isValidDayOfWeek("FRIDAY"));
      ASSERT(!isValidDayOfWeek(""));
      ASSERT(!isValidDayOfWeek("SND"));
  }
  
  TEST(ParseDayOfWeek, Basic) {
      ASSERT(DayOfWeek::thursday == parseDayOfWeek("thursday"));
      ASSERT(DayOfWeek::saturday == parseDayOfWeek("SAT"));
      ASSERT_THROWS_CODE(parseDayOfWeek(""), AssertionException, ErrorCodes::FailedToParse);
  }
  }  // namespace
  }  // namespace mongo
  