GenerateTimeZoneData.cpp 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  1. /*
  2. * Copyright (c) 2022, Tim Flynn <trflynn89@pm.me>
  3. *
  4. * SPDX-License-Identifier: BSD-2-Clause
  5. */
  6. #include "../LibUnicode/GeneratorUtil.h" // FIXME: Move this somewhere common.
  7. #include <AK/HashMap.h>
  8. #include <AK/SourceGenerator.h>
  9. #include <AK/String.h>
  10. #include <AK/StringBuilder.h>
  11. #include <AK/Vector.h>
  12. #include <LibCore/ArgsParser.h>
  13. #include <LibCore/File.h>
  14. namespace {
  15. struct Time {
  16. i8 hour { 0 };
  17. u8 minute { 0 };
  18. u8 second { 0 };
  19. };
  20. struct DateTime {
  21. u16 year { 0 };
  22. Optional<u8> month;
  23. Optional<u8> last_weekday;
  24. Optional<u8> after_weekday;
  25. Optional<u8> day;
  26. Optional<Time> time;
  27. };
  28. struct TimeZone {
  29. Time offset;
  30. Optional<DateTime> until;
  31. };
  32. struct TimeZoneData {
  33. HashMap<String, Vector<TimeZone>> time_zones;
  34. Vector<String> time_zones_names;
  35. Vector<Alias> time_zones_aliases;
  36. };
  37. static Time parse_time(StringView segment)
  38. {
  39. // FIXME: Some times end with a letter, e.g. "2:00u" and "2:00s". Figure out what this means and handle it.
  40. auto segments = segment.split_view(':');
  41. Time time {};
  42. time.hour = segments[0].to_int().value();
  43. time.minute = segments.size() > 1 ? segments[1].substring_view(0, 2).to_uint().value() : 0;
  44. time.second = segments.size() > 2 ? segments[2].substring_view(0, 2).to_uint().value() : 0;
  45. return time;
  46. }
  47. static Optional<DateTime> parse_date_time(Span<StringView const> segments)
  48. {
  49. constexpr auto months = Array { "Jan"sv, "Feb"sv, "Mar"sv, "Apr"sv, "May"sv, "Jun"sv, "Jul"sv, "Aug"sv, "Sep"sv, "Oct"sv, "Nov"sv, "Dec"sv };
  50. constexpr auto weekdays = Array { "Sun"sv, "Mon"sv, "Tue"sv, "Wed"sv, "Thu"sv, "Fri"sv, "Sat"sv };
  51. auto comment_index = find_index(segments.begin(), segments.end(), "#"sv);
  52. if (comment_index != segments.size())
  53. segments = segments.slice(0, comment_index);
  54. if (segments.is_empty())
  55. return {};
  56. DateTime date_time {};
  57. date_time.year = segments[0].to_uint().value();
  58. if (segments.size() > 1)
  59. date_time.month = find_index(months.begin(), months.end(), segments[1]);
  60. if (segments.size() > 2) {
  61. if (segments[2].starts_with("last"sv)) {
  62. auto weekday = segments[2].substring_view("last"sv.length());
  63. date_time.last_weekday = find_index(weekdays.begin(), weekdays.end(), weekday);
  64. } else if (auto index = segments[2].find(">="sv); index.has_value()) {
  65. auto weekday = segments[2].substring_view(0, *index);
  66. date_time.after_weekday = find_index(weekdays.begin(), weekdays.end(), weekday);
  67. auto day = segments[2].substring_view(*index + ">="sv.length());
  68. date_time.day = day.to_uint().value();
  69. } else {
  70. date_time.day = segments[2].to_uint().value();
  71. }
  72. }
  73. if (segments.size() > 3)
  74. date_time.time = parse_time(segments[3]);
  75. return date_time;
  76. }
  77. static Vector<TimeZone>& parse_zone(StringView zone_line, TimeZoneData& time_zone_data)
  78. {
  79. auto segments = zone_line.split_view_if([](char ch) { return (ch == '\t') || (ch == ' '); });
  80. // "Zone" NAME STDOFF RULES FORMAT [UNTIL]
  81. VERIFY(segments[0] == "Zone"sv);
  82. auto name = segments[1];
  83. TimeZone time_zone {};
  84. time_zone.offset = parse_time(segments[2]);
  85. if (segments.size() > 5)
  86. time_zone.until = parse_date_time(segments.span().slice(5));
  87. auto& time_zones = time_zone_data.time_zones.ensure(name);
  88. time_zones.append(move(time_zone));
  89. if (!time_zone_data.time_zones_names.contains_slow(name))
  90. time_zone_data.time_zones_names.append(name);
  91. return time_zones;
  92. }
  93. static void parse_zone_continuation(StringView zone_line, Vector<TimeZone>& time_zones)
  94. {
  95. auto segments = zone_line.split_view_if([](char ch) { return (ch == '\t') || (ch == ' '); });
  96. // STDOFF RULES FORMAT [UNTIL]
  97. TimeZone time_zone {};
  98. time_zone.offset = parse_time(segments[0]);
  99. if (segments.size() > 3)
  100. time_zone.until = parse_date_time(segments.span().slice(3));
  101. time_zones.append(move(time_zone));
  102. }
  103. static void parse_link(StringView link_line, TimeZoneData& time_zone_data)
  104. {
  105. auto segments = link_line.split_view_if([](char ch) { return (ch == '\t') || (ch == ' '); });
  106. // Link TARGET LINK-NAME
  107. VERIFY(segments[0] == "Link"sv);
  108. auto target = segments[1];
  109. auto alias = segments[2];
  110. time_zone_data.time_zones_aliases.append({ target, alias });
  111. }
  112. static ErrorOr<void> parse_time_zones(StringView time_zone_path, TimeZoneData& time_zone_data)
  113. {
  114. // For reference, the man page for `zic` has the best documentation of the TZDB file format.
  115. auto file = TRY(Core::File::open(time_zone_path, Core::OpenMode::ReadOnly));
  116. Vector<TimeZone>* last_parsed_zone = nullptr;
  117. while (file->can_read_line()) {
  118. auto line = file->read_line();
  119. if (line.is_empty() || line.trim_whitespace(TrimMode::Left).starts_with('#'))
  120. continue;
  121. if (line.starts_with("Zone"sv)) {
  122. last_parsed_zone = &parse_zone(line, time_zone_data);
  123. } else if (line.starts_with('\t')) {
  124. VERIFY(last_parsed_zone != nullptr);
  125. parse_zone_continuation(line, *last_parsed_zone);
  126. } else {
  127. last_parsed_zone = nullptr;
  128. if (line.starts_with("Link"sv))
  129. parse_link(line, time_zone_data);
  130. }
  131. }
  132. return {};
  133. }
  134. static String format_identifier(StringView owner, String identifier)
  135. {
  136. constexpr auto gmt_time_zones = Array { "Etc/GMT"sv, "GMT"sv };
  137. for (auto gmt_time_zone : gmt_time_zones) {
  138. if (identifier.starts_with(gmt_time_zone)) {
  139. auto offset = identifier.substring_view(gmt_time_zone.length());
  140. if (offset.starts_with('+'))
  141. identifier = String::formatted("{}_P{}", gmt_time_zone, offset.substring_view(1));
  142. else if (offset.starts_with('-'))
  143. identifier = String::formatted("{}_M{}", gmt_time_zone, offset.substring_view(1));
  144. }
  145. }
  146. identifier = identifier.replace("-"sv, "_"sv, true);
  147. identifier = identifier.replace("/"sv, "_"sv, true);
  148. if (all_of(identifier, is_ascii_digit))
  149. return String::formatted("{}_{}", owner[0], identifier);
  150. if (is_ascii_lower_alpha(identifier[0]))
  151. return String::formatted("{:c}{}", to_ascii_uppercase(identifier[0]), identifier.substring_view(1));
  152. return identifier;
  153. }
  154. static void generate_time_zone_data_header(Core::File& file, TimeZoneData& time_zone_data)
  155. {
  156. StringBuilder builder;
  157. SourceGenerator generator { builder };
  158. generator.append(R"~~~(
  159. #pragma once
  160. #include <AK/Types.h>
  161. namespace TimeZone {
  162. )~~~");
  163. generate_enum(generator, format_identifier, "TimeZone"sv, {}, time_zone_data.time_zones_names, time_zone_data.time_zones_aliases);
  164. generator.append(R"~~~(
  165. }
  166. )~~~");
  167. VERIFY(file.write(generator.as_string_view()));
  168. }
  169. static void generate_time_zone_data_implementation(Core::File& file, TimeZoneData& time_zone_data)
  170. {
  171. StringBuilder builder;
  172. SourceGenerator generator { builder };
  173. generator.append(R"~~~(
  174. #include <AK/Array.h>
  175. #include <AK/BinarySearch.h>
  176. #include <AK/Optional.h>
  177. #include <AK/StringView.h>
  178. #include <LibTimeZone/TimeZone.h>
  179. #include <LibTimeZone/TimeZoneData.h>
  180. namespace TimeZone {
  181. )~~~");
  182. auto append_from_string = [&](StringView enum_title, StringView enum_snake, auto const& values, auto const& aliases) {
  183. HashValueMap<String> hashes;
  184. hashes.ensure_capacity(values.size());
  185. for (auto const& value : values)
  186. hashes.set(value.hash(), format_identifier(enum_title, value));
  187. for (auto const& alias : aliases)
  188. hashes.set(alias.alias.hash(), format_identifier(enum_title, alias.alias));
  189. generate_value_from_string(generator, "{}_from_string"sv, enum_title, enum_snake, move(hashes));
  190. };
  191. append_from_string("TimeZone"sv, "time_zone"sv, time_zone_data.time_zones_names, time_zone_data.time_zones_aliases);
  192. generator.append(R"~~~(
  193. }
  194. )~~~");
  195. VERIFY(file.write(generator.as_string_view()));
  196. }
  197. }
  198. ErrorOr<int> serenity_main(Main::Arguments arguments)
  199. {
  200. StringView generated_header_path;
  201. StringView generated_implementation_path;
  202. Vector<StringView> time_zone_paths;
  203. Core::ArgsParser args_parser;
  204. args_parser.add_option(generated_header_path, "Path to the time zone data header file to generate", "generated-header-path", 'h', "generated-header-path");
  205. args_parser.add_option(generated_implementation_path, "Path to the time zone data implementation file to generate", "generated-implementation-path", 'c', "generated-implementation-path");
  206. args_parser.add_positional_argument(time_zone_paths, "Paths to the time zone database files", "time-zone-paths");
  207. args_parser.parse(arguments);
  208. auto open_file = [&](StringView path) -> ErrorOr<NonnullRefPtr<Core::File>> {
  209. if (path.is_empty()) {
  210. args_parser.print_usage(stderr, arguments.argv[0]);
  211. return Error::from_string_literal("Must provide all command line options"sv);
  212. }
  213. return Core::File::open(path, Core::OpenMode::ReadWrite);
  214. };
  215. auto generated_header_file = TRY(open_file(generated_header_path));
  216. auto generated_implementation_file = TRY(open_file(generated_implementation_path));
  217. TimeZoneData time_zone_data {};
  218. for (auto time_zone_path : time_zone_paths)
  219. TRY(parse_time_zones(time_zone_path, time_zone_data));
  220. generate_time_zone_data_header(generated_header_file, time_zone_data);
  221. generate_time_zone_data_implementation(generated_implementation_file, time_zone_data);
  222. return 0;
  223. }