llvm/libcxx/test/libcxx/time/time.zone/time.zone.db/rules.pass.cpp

//===----------------------------------------------------------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//

// UNSUPPORTED: c++03, c++11, c++14, c++17
// UNSUPPORTED: no-filesystem, no-localization, no-tzdb

// XFAIL: libcpp-has-no-experimental-tzdb
// XFAIL: availability-tzdb-missing

// <chrono>

// Tests the IANA database rules parsing and operations.
// This is not part of the public tzdb interface.
// The test uses private implementation headers.
// ADDITIONAL_COMPILE_FLAGS: -I %{libcxx-dir}/src/experimental/include

#include <chrono>
#include <fstream>
#include <string>
#include <string_view>
#include <variant>

#include "assert_macros.h"
#include "concat_macros.h"
#include "filesystem_test_helper.h"
#include "test_tzdb.h"

// headers in the dylib
#include "tzdb/types_private.h"
#include "tzdb/tzdb_private.h"

scoped_test_env env;
[[maybe_unused]] const std::filesystem::path dir = env.create_dir("zoneinfo");
const std::filesystem::path file                 = env.create_file("zoneinfo/tzdata.zi");

std::string_view std::chrono::__libcpp_tzdb_directory() {
  static std::string result = dir.string();
  return result;
}

static void write(std::string_view input) {
  static int version = 0;

  std::ofstream f{file};
  f << "# version " << version++ << '\n';
  f.write(input.data(), input.size());
}

struct parse_result {
  explicit parse_result(std::string_view input) {
    write(input);
    std::chrono::tzdb tzdb; // result not needed for the tests.
    std::chrono::__init_tzdb(tzdb, rules);
  }
  std::chrono::__tz::__rules_storage_type rules;
};

static void test_exception(std::string_view input, [[maybe_unused]] std::string_view what) {
  write(input);

  TEST_VALIDATE_EXCEPTION(
      std::runtime_error,
      [&]([[maybe_unused]] const std::runtime_error& e) {
        TEST_LIBCPP_REQUIRE(
            e.what() == what,
            TEST_WRITE_CONCATENATED("\nExpected exception ", what, "\nActual exception   ", e.what(), '\n'));
      },
      TEST_IGNORE_NODISCARD std::chrono::reload_tzdb());
}

static void test_invalid() {
  test_exception("R", "corrupt tzdb: expected whitespace");

  test_exception("R ", "corrupt tzdb: expected a string");

  test_exception("R r", "corrupt tzdb: expected whitespace");

  test_exception("R r x", "corrupt tzdb: expected a digit");
  test_exception("R r +", "corrupt tzdb: expected a digit");
  test_exception("R r mx", "corrupt tzdb year: expected 'min' or 'max'");
  test_exception("R r -32768", "corrupt tzdb year: year is less than the minimum");
  test_exception("R r 32768", "corrupt tzdb year: year is greater than the maximum");

  test_exception("R r mix", "corrupt tzdb: expected whitespace");
  test_exception("R r 0", "corrupt tzdb: expected whitespace");

  test_exception("R r 0 x", "corrupt tzdb: expected a digit");
  test_exception("R r 0 +", "corrupt tzdb: expected a digit");
  test_exception("R r 0 mx", "corrupt tzdb year: expected 'min' or 'max'");

  test_exception("R r 0 mix", "corrupt tzdb: expected whitespace");
  test_exception("R r 0 1", "corrupt tzdb: expected whitespace");

  test_exception("R r 0 1 X", "corrupt tzdb: expected character '-'");

  test_exception("R r 0 1 -", "corrupt tzdb: expected whitespace");

  test_exception("R r 0 1 - j", "corrupt tzdb month: invalid name");

  test_exception("R r 0 1 - Ja", "corrupt tzdb: expected whitespace");

  test_exception("R r 0 1 - Ja +", "corrupt tzdb weekday: invalid name");
  test_exception("R r 0 1 - Ja 32", "corrupt tzdb day: value too large");
  test_exception("R r 0 1 - Ja l", "corrupt tzdb: expected string 'last'");
  test_exception("R r 0 1 - Ja last", "corrupt tzdb weekday: invalid name");
  test_exception("R r 0 1 - Ja lastS", "corrupt tzdb weekday: invalid name");
  test_exception("R r 0 1 - Ja S", "corrupt tzdb weekday: invalid name");
  test_exception("R r 0 1 - Ja Su", "corrupt tzdb on: expected '>=' or '<='");
  test_exception("R r 0 1 - Ja Su>", "corrupt tzdb: expected character '='");
  test_exception("R r 0 1 - Ja Su<", "corrupt tzdb: expected character '='");
  test_exception("R r 0 1 - Ja Su>=+", "corrupt tzdb: expected a non-zero digit");
  test_exception("R r 0 1 - Ja Su>=0", "corrupt tzdb: expected a non-zero digit");
  test_exception("R r 0 1 - Ja Su>=32", "corrupt tzdb day: value too large");

  test_exception("R r 0 1 - Ja Su>=31", "corrupt tzdb: expected whitespace");

  test_exception("R r 0 1 - Ja Su>=31 ", "corrupt tzdb: expected a digit");
  test_exception("R r 0 1 - Ja Su>=31 +", "corrupt tzdb: expected a digit");

  test_exception("R r 0 1 - Ja Su>=31 1", "corrupt tzdb: expected whitespace");
  test_exception("R r 0 1 - Ja Su>=31 1a", "corrupt tzdb: expected whitespace");

  test_exception("R r 0 1 - Ja Su>=31 1w 2", "corrupt tzdb: expected whitespace");
  test_exception("R r 0 1 - Ja Su>=31 1w 2a", "corrupt tzdb: expected whitespace");

  test_exception("R r 0 1 - Ja Su>=31 1w 2s", "corrupt tzdb: expected whitespace");
  test_exception("R r 0 1 - Ja Su>=31 1w 2s ", "corrupt tzdb: expected a string");
}

static void test_name() {
  parse_result result{
      R"(
R z 0 1 - Ja Su>=31 1w 2s -
rULE z 0 1 - Ja Su>=31 1w 2s -
RuLe z 0 1 - Ja Su>=31 1w 2s -
R a 0 1 - Ja Su>=31 1w 2s -
R a 0 1 - Ja Su>=31 1w 2s -
)"};

  assert(result.rules.size() == 2);
  assert(result.rules[0].first == "a");
  assert(result.rules[0].second.size() == 2);
  assert(result.rules[1].first == "z");
  assert(result.rules[1].second.size() == 3);
}

static void test_from() {
  parse_result result{
      R"(
# min abbreviations
R a M 1 - Ja Su>=31 1w 2s -
R a mI 1 - Ja Su>=31 1w 2s -
R a mIN 1 - Ja Su>=31 1w 2s -

# max abbrviations
R a MA 1 - Ja Su>=31 1w 2s -
R a mAx 1 - Ja Su>=31 1w 2s -

R a -1000 1 - Ja Su>=31 1w 2s -
R a -100 1 - Ja Su>=31 1w 2s -
R a 0000 1 - Ja Su>=31 1w 2s -
R a 100 1 - Ja Su>=31 1w 2s -
R a 1000 1 - Ja Su>=31 1w 2s -
)"};

  assert(result.rules.size() == 1);
  assert(result.rules[0].second.size() == 10);

  assert(result.rules[0].second[0].__from == std::chrono::year::min());
  assert(result.rules[0].second[1].__from == std::chrono::year::min());
  assert(result.rules[0].second[2].__from == std::chrono::year::min());

  assert(result.rules[0].second[3].__from == std::chrono::year::max());
  assert(result.rules[0].second[4].__from == std::chrono::year::max());

  assert(result.rules[0].second[5].__from == std::chrono::year(-1000));
  assert(result.rules[0].second[6].__from == std::chrono::year(-100));
  assert(result.rules[0].second[7].__from == std::chrono::year(0));
  assert(result.rules[0].second[8].__from == std::chrono::year(100));
  assert(result.rules[0].second[9].__from == std::chrono::year(1000));
}

static void test_to() {
  parse_result result{
      R"(
# min abbreviations
R a 0 m - Ja Su>=31 1w 2s -
R a 0 mi - Ja Su>=31 1w 2s -
R a 0 min - Ja Su>=31 1w 2s -

# max abbrviations
R a 0 ma - Ja Su>=31 1w 2s -
R a 0 max - Ja Su>=31 1w 2s -

R a 0 -1000 - Ja Su>=31 1w 2s -
R a 0 -100 - Ja Su>=31 1w 2s -
R a 0 0000 - Ja Su>=31 1w 2s -
R a 0 100 - Ja Su>=31 1w 2s -
R a 0 1000 - Ja Su>=31 1w 2s -

# only abbreviations
R a m O - Ja Su>=31 1w 2s -
R a ma oN - Ja Su>=31 1w 2s -
R a -100 onL - Ja Su>=31 1w 2s -
R a 100 oNlY - Ja Su>=31 1w 2s -
)"};

  assert(result.rules.size() == 1);
  assert(result.rules[0].second.size() == 14);

  assert(result.rules[0].second[0].__to == std::chrono::year::min());
  assert(result.rules[0].second[1].__to == std::chrono::year::min());
  assert(result.rules[0].second[2].__to == std::chrono::year::min());

  assert(result.rules[0].second[3].__to == std::chrono::year::max());
  assert(result.rules[0].second[4].__to == std::chrono::year::max());

  assert(result.rules[0].second[5].__to == std::chrono::year(-1000));
  assert(result.rules[0].second[6].__to == std::chrono::year(-100));
  assert(result.rules[0].second[7].__to == std::chrono::year(0));
  assert(result.rules[0].second[8].__to == std::chrono::year(100));
  assert(result.rules[0].second[9].__to == std::chrono::year(1000));

  assert(result.rules[0].second[10].__to == std::chrono::year::min());
  assert(result.rules[0].second[11].__to == std::chrono::year::max());
  assert(result.rules[0].second[12].__to == std::chrono::year(-100));
  assert(result.rules[0].second[13].__to == std::chrono::year(100));
}

static void test_in() {
  parse_result result{
      R"(
# All tests in alphabetic order to validate shortest unique abbreviation

# Shortest abbreviation valid
R s 0 1 - ap Su>=31 1w 2s -
R s 0 1 - au Su>=31 1w 2s -
R s 0 1 - d Su>=31 1w 2s -
R s 0 1 - f Su>=31 1w 2s -
R s 0 1 - ja Su>=31 1w 2s -
R s 0 1 - jul Su>=31 1w 2s -
R s 0 1 - jun Su>=31 1w 2s -
R s 0 1 - May Su>=31 1w 2s -
R s 0 1 - mar Su>=31 1w 2s -
R s 0 1 - n Su>=31 1w 2s -
R s 0 1 - o Su>=31 1w 2s -
R s 0 1 - s Su>=31 1w 2s -

# 3 letter abbreviation
R a 0 1 - APR Su>=31 1w 2s -
R a 0 1 - AUG Su>=31 1w 2s -
R a 0 1 - DEC Su>=31 1w 2s -
R a 0 1 - FEB Su>=31 1w 2s -
R a 0 1 - JAN Su>=31 1w 2s -
R a 0 1 - JUL Su>=31 1w 2s -
R a 0 1 - JUN Su>=31 1w 2s -
R a 0 1 - MAY Su>=31 1w 2s -
R a 0 1 - MAR Su>=31 1w 2s -
R a 0 1 - NOV Su>=31 1w 2s -
R a 0 1 - OCT Su>=31 1w 2s -
R a 0 1 - SEP Su>=31 1w 2s -

# Full names
R f 0 1 - ApRiL Su>=31 1w 2s -
R f 0 1 - AuGuSt Su>=31 1w 2s -
R f 0 1 - DeCeMber Su>=31 1w 2s -
R f 0 1 - FeBrUary Su>=31 1w 2s -
R f 0 1 - JaNuAry Su>=31 1w 2s -
R f 0 1 - JuLy Su>=31 1w 2s -
R f 0 1 - JuNe Su>=31 1w 2s -
R f 0 1 - MaY Su>=31 1w 2s -
R f 0 1 - MaRch Su>=31 1w 2s -
R f 0 1 - NoVemBeR Su>=31 1w 2s -
R f 0 1 - OcTobEr Su>=31 1w 2s -
R f 0 1 - SePteMbEr Su>=31 1w 2s -
)"};

  assert(result.rules.size() == 3);
  for (std::size_t i = 0; i < result.rules.size(); ++i) {
    assert(result.rules[i].second.size() == 12);

    assert(result.rules[i].second[0].__in == std::chrono::April);
    assert(result.rules[i].second[1].__in == std::chrono::August);
    assert(result.rules[i].second[2].__in == std::chrono::December);
    assert(result.rules[i].second[3].__in == std::chrono::February);
    assert(result.rules[i].second[4].__in == std::chrono::January);
    assert(result.rules[i].second[5].__in == std::chrono::July);
    assert(result.rules[i].second[6].__in == std::chrono::June);
    assert(result.rules[i].second[7].__in == std::chrono::May);
    assert(result.rules[i].second[8].__in == std::chrono::March);
    assert(result.rules[i].second[9].__in == std::chrono::November);
    assert(result.rules[i].second[10].__in == std::chrono::October);
    assert(result.rules[i].second[11].__in == std::chrono::September);
  }
};

static void test_on_day() {
  parse_result result{
      R"(
# The parser does not validate the day as valid day of month
R a 0 1 - Fe 1 1w 2s -
R a 0 1 - Fe 10 1w 2s -
R a 0 1 - Fe 20 1w 2s -
R a 0 1 - Fe 30 1w 2s -
R a 0 1 - Fe 31 1w 2s -
)"};

  assert(result.rules.size() == 1);
  assert(result.rules[0].second.size() == 5);
  assert(std::get<std::chrono::day>(result.rules[0].second[0].__on) == std::chrono::day(1));
  assert(std::get<std::chrono::day>(result.rules[0].second[1].__on) == std::chrono::day(10));
  assert(std::get<std::chrono::day>(result.rules[0].second[2].__on) == std::chrono::day(20));
  assert(std::get<std::chrono::day>(result.rules[0].second[3].__on) == std::chrono::day(30));
  assert(std::get<std::chrono::day>(result.rules[0].second[4].__on) == std::chrono::day(31));
}

static void test_on_last() {
  parse_result result{
      R"(
# All tests in alphabetic order to validate shortest unique abbreviation

# Shortest abbreviation valid
R s 0 1 - Ja lastF 1w 2s -
R s 0 1 - Ja lastM 1w 2s -
R s 0 1 - Ja lastSa 1w 2s -
R s 0 1 - Ja lastSu 1w 2s -
R s 0 1 - Ja lastTh 1w 2s -
R s 0 1 - Ja lastTu 1w 2s -
R s 0 1 - Ja lastW 1w 2s -

# 3 letter abbreviation
R a 0 1 - Ja lastFri 1w 2s -
R a 0 1 - Ja lastMon 1w 2s -
R a 0 1 - Ja lastSat 1w 2s -
R a 0 1 - Ja lastSun 1w 2s -
R a 0 1 - Ja lastThu 1w 2s -
R a 0 1 - Ja lastTue 1w 2s -
R a 0 1 - Ja lastWed 1w 2s -

# Full names
R f 0 1 - Ja lastFriday 1w 2s -
R f 0 1 - Ja lastMonday 1w 2s -
R f 0 1 - Ja lastSaturday 1w 2s -
R f 0 1 - Ja lastSunday 1w 2s -
R f 0 1 - Ja lastThursday 1w 2s -
R f 0 1 - Ja lastTuesday 1w 2s -
R f 0 1 - Ja lastWednesday 1w 2s -
)"};

  assert(result.rules.size() == 3);
  for (std::size_t i = 0; i < result.rules.size(); ++i) {
    assert(result.rules[i].second.size() == 7);

    assert(std::get<std::chrono::weekday_last>(result.rules[i].second[0].__on) ==
           std::chrono::weekday_last(std::chrono::Friday));
    assert(std::get<std::chrono::weekday_last>(result.rules[i].second[1].__on) ==
           std::chrono::weekday_last(std::chrono::Monday));
    assert(std::get<std::chrono::weekday_last>(result.rules[i].second[2].__on) ==
           std::chrono::weekday_last(std::chrono::Saturday));
    assert(std::get<std::chrono::weekday_last>(result.rules[i].second[3].__on) ==
           std::chrono::weekday_last(std::chrono::Sunday));
    assert(std::get<std::chrono::weekday_last>(result.rules[i].second[4].__on) ==
           std::chrono::weekday_last(std::chrono::Thursday));
    assert(std::get<std::chrono::weekday_last>(result.rules[i].second[5].__on) ==
           std::chrono::weekday_last(std::chrono::Tuesday));
    assert(std::get<std::chrono::weekday_last>(result.rules[i].second[6].__on) ==
           std::chrono::weekday_last(std::chrono::Wednesday));
  }
}

static void test_on_constrain() {
  parse_result result{
      R"(
# Shortest abbreviation valid
R s 0 1 - Ja F>=1 1w 2s -
R s 0 1 - Ja M<=1 1w 2s -
R s 0 1 - Ja Sa>=31 1w 2s -
R s 0 1 - Ja Su<=31 1w 2s -
R s 0 1 - Ja Th>=10 1w 2s -
R s 0 1 - Ja Tu<=20 1w 2s -
R s 0 1 - Ja W>=30 1w 2s -

# 3 letter abbreviation
R a 0 1 - Ja Fri>=1 1w 2s -
R a 0 1 - Ja Mon<=1 1w 2s -
R a 0 1 - Ja Sat>=31 1w 2s -
R a 0 1 - Ja Sun<=31 1w 2s -
R a 0 1 - Ja Thu>=10 1w 2s -
R a 0 1 - Ja Tue<=20 1w 2s -
R a 0 1 - Ja Wed>=30 1w 2s -

# Full names
R f 0 1 - Ja Friday>=1 1w 2s -
R f 0 1 - Ja Monday<=1 1w 2s -
R f 0 1 - Ja Saturday>=31 1w 2s -
R f 0 1 - Ja Sunday<=31 1w 2s -
R f 0 1 - Ja Thursday>=10 1w 2s -
R f 0 1 - Ja Tuesday<=20 1w 2s -
R f 0 1 - Ja Wednesday>=30 1w 2s -

)"};

  std::chrono::__tz::__constrained_weekday r;
  assert(result.rules.size() == 3);
  for (std::size_t i = 0; i < result.rules.size(); ++i) {
    assert(result.rules[i].second.size() == 7);

    r = std::get<std::chrono::__tz::__constrained_weekday>(result.rules[i].second[0].__on);
    assert(r.__weekday == std::chrono::Friday);
    assert(r.__comparison == std::chrono::__tz::__constrained_weekday::__ge);
    assert(r.__day == std::chrono::day(1));

    r = std::get<std::chrono::__tz::__constrained_weekday>(result.rules[i].second[1].__on);
    assert(r.__weekday == std::chrono::Monday);
    assert(r.__comparison == std::chrono::__tz::__constrained_weekday::__le);
    assert(r.__day == std::chrono::day(1));

    r = std::get<std::chrono::__tz::__constrained_weekday>(result.rules[i].second[2].__on);
    assert(r.__weekday == std::chrono::Saturday);
    assert(r.__comparison == std::chrono::__tz::__constrained_weekday::__ge);
    assert(r.__day == std::chrono::day(31));

    r = std::get<std::chrono::__tz::__constrained_weekday>(result.rules[i].second[3].__on);
    assert(r.__weekday == std::chrono::Sunday);
    assert(r.__comparison == std::chrono::__tz::__constrained_weekday::__le);
    assert(r.__day == std::chrono::day(31));

    r = std::get<std::chrono::__tz::__constrained_weekday>(result.rules[i].second[4].__on);
    assert(r.__weekday == std::chrono::Thursday);
    assert(r.__comparison == std::chrono::__tz::__constrained_weekday::__ge);
    assert(r.__day == std::chrono::day(10));

    r = std::get<std::chrono::__tz::__constrained_weekday>(result.rules[i].second[5].__on);
    assert(r.__weekday == std::chrono::Tuesday);
    assert(r.__comparison == std::chrono::__tz::__constrained_weekday::__le);
    assert(r.__day == std::chrono::day(20));

    r = std::get<std::chrono::__tz::__constrained_weekday>(result.rules[i].second[6].__on);
    assert(r.__weekday == std::chrono::Wednesday);
    assert(r.__comparison == std::chrono::__tz::__constrained_weekday::__ge);
    assert(r.__day == std::chrono::day(30));
  }
}

static void test_on() {
  test_on_day();
  test_on_last();
  test_on_constrain();
}

static void test_at() {
  parse_result result{
      R"(
# Based on the examples in the man page.
# Note the input is not expected to have fractional seconds, they are truncated.
R a 0 1 - Ja Su>=31 2w 2s -
R a 0 1 - Ja Su>=31 2:00s 2s -
R a 0 1 - Ja Su>=31 01:28:14u 2s -
R a 0 1 - Ja Su>=31 00:19:32.10g 2s -
R a 0 1 - Ja Su>=31 12:00z 2s -
R a 0 1 - Ja Su>=31 15:00 2s -
R a 0 1 - Ja Su>=31 24:00 2s -
R a 0 1 - Ja Su>=31 260:00 2s -
R a 0 1 - Ja Su>=31 -2:30 2s -
R a 0 1 - Ja Su>=31 - 2s -
)"};

  assert(result.rules.size() == 1);
  assert(result.rules[0].second.size() == 10);

  assert(result.rules[0].second[0].__at.__time == std::chrono::hours(2));
  assert(result.rules[0].second[0].__at.__clock == std::chrono::__tz::__clock::__local);

  assert(result.rules[0].second[1].__at.__time == std::chrono::hours(2));
  assert(result.rules[0].second[1].__at.__clock == std::chrono::__tz::__clock::__standard);

  assert(result.rules[0].second[2].__at.__time ==
         std::chrono::hours(1) + std::chrono::minutes(28) + std::chrono::seconds(14));
  assert(result.rules[0].second[2].__at.__clock == std::chrono::__tz::__clock::__universal);

  assert(result.rules[0].second[3].__at.__time == std::chrono::minutes(19) + std::chrono::seconds(32));
  assert(result.rules[0].second[3].__at.__clock == std::chrono::__tz::__clock::__universal);

  assert(result.rules[0].second[4].__at.__time == std::chrono::hours(12));
  assert(result.rules[0].second[4].__at.__clock == std::chrono::__tz::__clock::__universal);

  assert(result.rules[0].second[5].__at.__time == std::chrono::hours(15));
  assert(result.rules[0].second[5].__at.__clock == std::chrono::__tz::__clock::__local);

  assert(result.rules[0].second[6].__at.__time == std::chrono::hours(24));
  assert(result.rules[0].second[6].__at.__clock == std::chrono::__tz::__clock::__local);

  assert(result.rules[0].second[7].__at.__time == std::chrono::hours(260));
  assert(result.rules[0].second[7].__at.__clock == std::chrono::__tz::__clock::__local);

  assert(result.rules[0].second[8].__at.__time == -(std::chrono::hours(2) + std::chrono::minutes(30)));
  assert(result.rules[0].second[8].__at.__clock == std::chrono::__tz::__clock::__local);

  assert(result.rules[0].second[9].__at.__time == std::chrono::hours(0)); // The man page expresses it in hours
  assert(result.rules[0].second[9].__at.__clock == std::chrono::__tz::__clock::__local);
}

static void test_save() {
  parse_result result{
      R"(
R a 0 1 - Ja Su>=31 1w 2d -
R a 0 1 - Ja Su>=31 1w 2:00s -
R a 0 1 - Ja Su>=31 1w 0 -
R a 0 1 - Ja Su>=31 1w 0:00:01 -
R a 0 1 - Ja Su>=31 1w -0:00:01 -
)"};

  assert(result.rules.size() == 1);
  assert(result.rules[0].second.size() == 5);

  assert(result.rules[0].second[0].__save.__time == std::chrono::hours(2));
  assert(result.rules[0].second[0].__save.__is_dst == true);

  assert(result.rules[0].second[1].__save.__time == std::chrono::hours(2));
  assert(result.rules[0].second[1].__save.__is_dst == false);

  assert(result.rules[0].second[2].__save.__time == std::chrono::hours(0));
  assert(result.rules[0].second[2].__save.__is_dst == false);

  assert(result.rules[0].second[3].__save.__time == std::chrono::seconds(1));
  assert(result.rules[0].second[3].__save.__is_dst == true);

  assert(result.rules[0].second[4].__save.__time == -std::chrono::seconds(1));
  assert(result.rules[0].second[4].__save.__is_dst == true);
}

static void test_letter() {
  parse_result result{
      R"(
R a 0 1 - Ja Su>=31 1w 2s -
R a 0 1 - Ja Su>=31 1w 2s a
R a 0 1 - Ja Su>=31 1w 2s abc
)"};

  assert(result.rules.size() == 1);
  assert(result.rules[0].second.size() == 3);

  assert(result.rules[0].second[0].__letters == "");
  assert(result.rules[0].second[1].__letters == "a");
  assert(result.rules[0].second[2].__letters == "abc");
}

static void test_mixed_order() {
  // This is a part of the real database. The interesting part is that the
  // rules NZ and Chatham are interleaved. Make sure the parse algorithm
  // handles this correctly.
  parse_result result{
      R"(
# Since 1957 Chatham has been 45 minutes ahead of NZ, but until 2018a
# there was no documented single notation for the date and time of this
# transition.  Duplicate the Rule lines for now, to give the 2018a change
# time to percolate out.
Rule NZ  1974    only    -   Nov Sun>=1  2:00s   1:00    D
Rule Chatham 1974    only    -   Nov Sun>=1  2:45s   1:00    -
Rule NZ  1975    only    -   Feb lastSun 2:00s   0   S
Rule Chatham 1975    only    -   Feb lastSun 2:45s   0   -
Rule NZ  1975    1988    -   Oct lastSun 2:00s   1:00    D
Rule Chatham 1975    1988    -   Oct lastSun 2:45s   1:00    -
)"};

  assert(result.rules.size() == 2);
  assert(result.rules[0].second.size() == 3);
  assert(result.rules[1].second.size() == 3);
}

int main(int, const char**) {
  test_invalid();
  test_name();
  test_from();
  test_to();
  test_in();
  test_on();
  test_at();
  test_save();
  test_letter();
  test_mixed_order();

  return 0;
}