├── .formatter.exs
├── .github
└── FUNDING.yml
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── bench
└── compiler.exs
├── lib
├── mix
│ └── tasks
│ │ └── download.ex
├── tz.ex
└── tz
│ ├── compiler.ex
│ ├── compiler_runner.ex
│ ├── http.ex
│ ├── http
│ ├── http_client.ex
│ ├── http_response.ex
│ └── mint
│ │ ├── http_client.ex
│ │ └── http_response.ex
│ ├── iana_data_dir.ex
│ ├── iana_file_parser.ex
│ ├── periods_builder.ex
│ ├── time_zone_database.ex
│ ├── update_periodically.ex
│ ├── updater.ex
│ └── watch_periodically.ex
├── mix.exs
├── mix.lock
├── priv
└── tzdata2024b
│ ├── africa
│ ├── antarctica
│ ├── asia
│ ├── australasia
│ ├── backward
│ ├── etcetera
│ ├── europe
│ ├── iso3166.tab
│ ├── northamerica
│ ├── southamerica
│ └── zone1970.tab
└── test
├── dynamic_periods_test.exs
├── support
└── holocene_calendar.ex
├── test_helper.exs
├── time_zone_database_test.exs
└── updater_test.exs
/.formatter.exs:
--------------------------------------------------------------------------------
1 | # Used by "mix format"
2 | [
3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
4 | ]
5 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: mathieuprog
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # asdf
2 | .tool-versions
3 |
4 | .history
5 |
6 | # IntelliJ IDEA
7 | /.idea/
8 | *.iml
9 |
10 | # The directory Mix will write compiled artifacts to.
11 | /_build/
12 |
13 | # If you run "mix test --cover", coverage assets end up here.
14 | /cover/
15 |
16 | # The directory Mix downloads your dependencies sources to.
17 | /deps/
18 |
19 | # Where third-party dependencies like ExDoc output generated docs.
20 | /doc/
21 |
22 | # Ignore .fetch files in case you like to edit your project deps locally.
23 | /.fetch
24 |
25 | # If the VM crashes, it generates a dump, let's ignore it too.
26 | erl_crash.dump
27 |
28 | # Also ignore archive artifacts (built via "mix archive.build").
29 | *.ez
30 |
31 | # Ignore package tarball (built via "mix hex.build").
32 | tz-*.tar
33 |
34 | # macOS
35 | .DS_Store
36 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## 0.28.x
4 |
5 | * Ensure periodic updater and watcher don't crash the app on GenServer init.
6 |
7 | ## 0.27.x
8 |
9 | * Add `Tz.PeriodsProvider.next_period/1`.
10 |
11 | ## 0.26.x
12 |
13 | * Add `:iana_version` config.
14 | * Address Elixir 1.15 warning messages.
15 | * Address Elixir 1.17 warning messages.
16 |
17 | ## 0.25.x
18 |
19 | * Add custom options for default Mint HTTP client.
20 | * Fix the bug that occurs when the maximum available year in an IANA rule exceeds
21 | the limit set by the 'max' rule (happened for Palestine).
22 | * Fix warnings.
23 |
24 | ## 0.24.x
25 |
26 | * Handle negative years.
27 | * Convert non-iso datetime to iso.
28 |
29 | ## 0.23.x
30 |
31 | * Change option names:
32 | * `:reject_time_zone_periods_before_year` to
33 | `:reject_periods_before_year`.
34 | * `:build_time_zone_periods_with_ongoing_dst_changes_until_year` to
35 | `:build_dst_periods_until_year`.
36 | * Add a mix task to download the IANA time zone data for a given version.
37 | * Fix warnings.
38 |
39 | ## 0.22.x
40 |
41 | * Add a mix task to download the IANA time zone data for a given version.
42 |
43 | ## 0.21.x
44 |
45 | * Allow to configure the schedulers:
46 | * `Tz.UpdatePeriodically` may receive the option `:interval_in_days`.
47 | * `Tz.WatchPeriodically` may receive the options `:interval_in_days` and `on_update`.
48 | * Fixed: schedulers were only running once.
49 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | Copyright 2020 Mathieu Decaffmeyer
179 |
180 | Licensed under the Apache License, Version 2.0 (the "License");
181 | you may not use this file except in compliance with the License.
182 | You may obtain a copy of the License at
183 |
184 | http://www.apache.org/licenses/LICENSE-2.0
185 |
186 | Unless required by applicable law or agreed to in writing, software
187 | distributed under the License is distributed on an "AS IS" BASIS,
188 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
189 | See the License for the specific language governing permissions and
190 | limitations under the License.
191 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Tz
2 |
3 | Time zone support for Elixir.
4 |
5 | The Elixir standard library does not ship with a time zone database. As a result, the functions in the `DateTime`
6 | module can, by default, only operate on datetimes in the UTC time zone. Alternatively (and
7 | [deliberately](https://elixirforum.com/t/14743)), the standard library relies on
8 | third-party libraries, such as `tz`, to bring in time zone support and deal with datetimes in other time zones than UTC.
9 |
10 | The `tz` library relies on the [time zone database](https://data.iana.org/time-zones/tzdb/) maintained by
11 | [IANA](https://www.iana.org). As of version 0.28.1, `tz` uses version **tzdata2024b** of the IANA time zone database.
12 |
13 | * [Installation and usage](#installation-and-usage)
14 | * [Core principles](#core-principles)
15 | * [Automatic updates](#automatic-time-zone-data-updates)
16 | * [Manual updates](#manual-time-zone-data-updates)
17 | * [Automatic vs manual updates](#automatic-vs-manual-updates)
18 | * [Disable updates in test env](#disable-updates-in-test-environment)
19 | * [Default HTTP client](#default-http-client)
20 | * [Custom HTTP client](#custom-http-client)
21 | * [Performance tweaks](#performance-tweaks)
22 | * [Custom storage location](#custom-storage-location-of-time-zone-data)
23 | * [Get the IANA version](#get-the-iana-time-zone-database-version)
24 | * [Time zone utility functions](#time-zone-utility-functions)
25 | * [Other libraries](#other-time-zone-database-implementations)
26 | * [Acknowledgments](#acknowledgments)
27 |
28 | ## Installation and usage
29 |
30 | Add `tz` for Elixir as a dependency in your `mix.exs` file:
31 |
32 | ```elixir
33 | def deps do
34 | [
35 | {:tz, "~> 0.28"}
36 | ]
37 | end
38 | ```
39 |
40 | To use the `tz` database, either configure it via configuration:
41 | ```elixir
42 | config :elixir, :time_zone_database, Tz.TimeZoneDatabase
43 | ```
44 |
45 | or by calling `Calendar.put_time_zone_database/1`:
46 | ```elixir
47 | Calendar.put_time_zone_database(Tz.TimeZoneDatabase)
48 | ```
49 |
50 | or by passing the module name `Tz.TimeZoneDatabase` directly to the functions that need a time zone database:
51 | ```elixir
52 | DateTime.now("America/Sao_Paulo", Tz.TimeZoneDatabase)
53 | ```
54 |
55 | Refer to the [DateTime API](https://hexdocs.pm/elixir/DateTime.html) for more details
56 | about handling datetimes with time zones.
57 |
58 | ## Core principles
59 |
60 | ### Battle-tested
61 |
62 | The `tz` library is tested against nearly 10 million past dates, which includes most of all possible edge cases.
63 |
64 | The repo [tzdb_test](https://github.com/mathieuprog/tzdb_test) compares the output of the different available libraries (tz, time_zone_info, tzdata and zoneinfo), and gives some idea of the difference in performance.
65 |
66 | ### Pre-compiled time zone data
67 |
68 | Time zone periods are deducted from the [IANA time zone data](https://data.iana.org/time-zones/tzdb/). A period is a
69 | period of time where a certain offset is observed. For example, in Belgium from 31 March 2019 until 27 October 2019, clock
70 | went forward by 1 hour; as Belgium has a base offset from UTC of 1 hour, this means that during this period, Belgium observed a total offset of 2 hours from UTC time (base UTC offset of 1 hour + DST offset of 1 hour).
71 |
72 | The time zone periods are computed and made available in Elixir maps during compilation time, to be consumed by the
73 | [DateTime](https://hexdocs.pm/elixir/DateTime.html#module-time-zone-database) module.
74 |
75 | ## Automatic time zone data updates
76 |
77 | `tz` can watch for IANA time zone database updates and automatically recompile the time zone periods.
78 |
79 | To enable automatic updates, add `Tz.UpdatePeriodically` as a child in your supervisor:
80 |
81 | ```elixir
82 | {Tz.UpdatePeriodically, []}
83 | ```
84 |
85 | You may pass the option `:interval_in_days` in order to configure the frequency of the updates:
86 |
87 | ```elixir
88 | {Tz.UpdatePeriodically, [interval_in_days: 5]}
89 | ```
90 |
91 | ## Manual time zone data updates
92 |
93 | If you do not wish to update automatically, but still wish to be alerted for new upcoming IANA updates, add
94 | `Tz.WatchPeriodically` as a child in your supervisor:
95 |
96 | ```elixir
97 | {Tz.WatchPeriodically, []}
98 | ```
99 |
100 | `Tz.WatchPeriodically` simply logs to your server when a new time zone database is available.
101 |
102 | You may pass the options:
103 | * `:interval_in_days`: frequency of the checks
104 | * `:on_update`: a user callback executed when an update is available
105 |
106 | For updating IANA data manually, there are 2 options:
107 |
108 | * just update the `tz` library in the `mix.exs` file, which hopefully includes the latest IANA time zone database (if not, wait for the library maintainer to include the latest version or send a pull request on GitHub).
109 |
110 | * download the files and recompile:
111 |
112 | 1. Configure a custom directory with the `:data_dir` option. For example:
113 | ```elixir
114 | config :tz, :data_dir, Path.join(Path.dirname(__DIR__), "priv")
115 | ```
116 | 2. Download the files manually by running the mix task below:
117 | ```bash
118 | mix tz.download
119 | ```
120 | 3. Recompile the dependency:
121 | ```bash
122 | mix deps.compile tz --force
123 | ```
124 | Or from an iex session to recompile at runtime:
125 | ```bash
126 | iex -S mix
127 | iex> Tz.Compiler.compile()
128 | ```
129 | Note that recompilation at runtime is not persistent, run `mix deps.compile tz --force` in addition.
130 | 4. Check that the version is the one expected:
131 | ```bash
132 | iex> Tz.iana_version()
133 | ```
134 |
135 | To force a specific IANA version:
136 |
137 | 1. Configure a custom directory with the `:data_dir` option. For example:
138 | ```elixir
139 | config :tz, :data_dir, Path.join(Path.dirname(__DIR__), "priv")
140 | ```
141 | 2. Download the files by running the mix task below (say we want the 2021a version):
142 | ```bash
143 | mix tz.download 2021a
144 | ```
145 | 3. Add the `:iana_version` option:
146 | ```elixir
147 | config :tz, :iana_version, "2021a"
148 | ```
149 | 4. Recompile the dependency:
150 | ```bash
151 | mix deps.compile tz --force
152 | ```
153 | 5. Check that the version is the one expected:
154 | ```bash
155 | iex> Tz.iana_version()
156 | ```
157 |
158 | ## Automatic vs manual updates
159 |
160 | Some users prefer to use `Tz.WatchPeriodically` (over `Tz.UpdatePeriodically`) to watch and update manually. Example cases:
161 |
162 | * Memory-limited systems: small containers or embedded devices may not afford to recompile the time zone data at runtime.
163 | * Restricted environments: the request may be blocked because of security policies.
164 | * Security concerns: some users may prefer to analyze the files coming from external sources (`https://data.iana.org` in this case) before processing.
165 | * Systems interoperability: a user may use some other systems using an older version of the IANA database, and so the user may want to keep a lower version of the IANA data with `tz` to ensure IANA versions match.
166 |
167 | ## Disable updates in test environment
168 |
169 | To avoid the updater to run while executing tests, you may conditionally add the child worker in your supervisor. For example:
170 |
171 | ```elixir
172 | children = [
173 | MyApp.Repo,
174 | MyApp.Endpoint,
175 | #...
176 | ]
177 | |> append_if(Application.get_env(:my_app, :env) != :test, {Tz.UpdatePeriodically, []})
178 | ```
179 |
180 | ```elixir
181 | defp append_if(list, condition, item) do
182 | if condition, do: list ++ [item], else: list
183 | end
184 | ```
185 |
186 | In `config.exs`, add `config :my_app, env: Mix.env()`.
187 |
188 | ## Default HTTP client
189 |
190 | Lastly, add the http client `mint` and ssl certificate store `castore` into your `mix.exs` file:
191 |
192 | ```elixir
193 | defp deps do
194 | [
195 | {:castore, "~> 1.0"},
196 | {:mint, "~> 1.6"},
197 | {:tz, "~> 0.28"}
198 | ]
199 | end
200 | ```
201 |
202 | You may also add custom [options](https://hexdocs.pm/mint/Mint.HTTP.html#connect/4-options) for the http client `mint`:
203 |
204 | ```elixir
205 | config :tz, Tz.HTTP.Mint.HTTPClient,
206 | proxy: {:http, proxy_host, proxy_port, []}
207 | ```
208 |
209 | ## Custom HTTP client
210 |
211 | You may implement the `Tz.HTTP.HTTPClient` behaviour in order to use another HTTP client.
212 |
213 | Example using [Finch](https://github.com/keathley/finch):
214 | ```elixir
215 | defmodule MyApp.Tz.HTTPClient do
216 | @behaviour Tz.HTTP.HTTPClient
217 |
218 | alias Tz.HTTP.HTTPResponse
219 | alias MyApp.MyFinch
220 |
221 | @impl Tz.HTTP.HTTPClient
222 | def request(hostname, path) do
223 | {:ok, response} =
224 | Finch.build(:get, "https://" <> Path.join(hostname, path))
225 | |> Finch.request(MyFinch)
226 |
227 | %HTTPResponse{
228 | status_code: response.status,
229 | body: response.body
230 | }
231 | end
232 | end
233 | ```
234 |
235 | A `Tz.HTTP.HTTPResponse` struct must be returned with fields `:status_code` and `:body`.
236 |
237 | The custom module must then be passed into the config:
238 | ```elixir
239 | config :tz, :http_client, MyApp.Tz.HTTPClient
240 | ```
241 |
242 | ## Performance tweaks
243 |
244 | `tz` accepts two environment options to tweak performance.
245 |
246 | ### Reducing period lookup time
247 |
248 | For time zones that have ongoing DST changes, period lookups for dates far in the future result in periods being
249 | dynamically computed based on the IANA data. For example, what is the period for 20 March 2040 for New York (let's
250 | assume that the last rules for New York still mention an ongoing DST change as you read this)? We can't compile periods
251 | indefinitely in the future; by default, such periods are computed until 5 years from compilation time. Dynamic period
252 | computations is a slow operation.
253 |
254 | You can decrease **period lookup time** for time zones affected by DST changes, by specifying until what year those periods have to be computed:
255 |
256 | ```elixir
257 | config :tz, build_dst_periods_until_year: 20 + NaiveDateTime.utc_now().year
258 | ```
259 |
260 | Note that increasing the year will also slightly increase compilation time, as it generates more periods to compile.
261 |
262 | The default setting computes periods for a period of 5 years from the time the code is compiled. Note that if you have added the automatic updater, the periods will be recomputed with every update, which occurs multiple times throughout the year.
263 |
264 | ### Rejecting old time zone periods
265 |
266 | You can slightly decrease **memory usage** and **compilation time**, by rejecting time zone periods before a given year:
267 |
268 | ```elixir
269 | config :tz, reject_periods_before_year: 2010
270 | ```
271 |
272 | Note that this option is aimed towards embedded devices as the difference should be insignificant for ordinary servers.
273 |
274 | By default, no periods are rejected.
275 |
276 | ## Custom storage location of time zone data
277 |
278 | By default, the files are stored in the `priv` directory of the `tz` library. You may customize the directory that will hold all of the IANA timezone data. For example, if you want to store the files in your project's `priv` dir instead:
279 |
280 | ```elixir
281 | config :tz, :data_dir, Path.join(Path.dirname(__DIR__), "priv")
282 | ```
283 |
284 | ## Get the IANA time zone database version
285 |
286 | ```elixir
287 | Tz.iana_version() == "2023c"
288 | ```
289 |
290 | ## Time zone utility functions
291 |
292 | Tz's API is intentionally kept as minimal as possible to implement Calendar.TimeZoneDatabase's behaviour. Utility functions
293 | around time zones are provided by [TzExtra](https://github.com/mathieuprog/tz_extra).
294 |
295 | * [`TzExtra.countries_time_zones/1`](https://github.com/mathieuprog/tz_extra#tzextracountries_time_zones1): returns a list of time zone data by country
296 | * [`TzExtra.time_zone_identifiers/1`](https://github.com/mathieuprog/tz_extra#tzextratime_zone_identifiers1): returns a list of time zone identifiers
297 | * [`TzExtra.civil_time_zone_identifiers/1`](https://github.com/mathieuprog/tz_extra#tzextracivil_time_zone_identifiers1): returns a list of time zone identifiers that are tied to a country
298 | * [`TzExtra.countries/0`](https://github.com/mathieuprog/tz_extra#tzextracountries0): returns a list of ISO country codes with their English name
299 | * [`TzExtra.get_canonical_time_zone_identifier/1`](https://github.com/mathieuprog/tz_extra#tzextraget_canonical_time_zone_identifier1): returns the canonical time zone identifier for the given time zone identifier
300 | * [`TzExtra.Changeset.validate_time_zone_identifier/3`](https://github.com/mathieuprog/tz_extra#tzextraChangesetvalidate_time_zone_identifier3): an Ecto Changeset validator, validating that the user input is a valid time zone
301 | * [`TzExtra.Changeset.validate_civil_time_zone_identifier/3`](https://github.com/mathieuprog/tz_extra#tzextraChangesetvalidate_civil_time_zone_identifier3): an Ecto Changeset validator, validating that the user input is a valid civil time zone
302 | * [`TzExtra.Changeset.validate_iso_country_code/3`](https://github.com/mathieuprog/tz_extra#tzextraChangesetvalidate_iso_country_code3): an Ecto Changeset validator, validating that the user input is a valid ISO country code
303 |
304 | ## Other time zone database implementations
305 |
306 | ### Based on IANA time zone data
307 |
308 | * [time_zone_info](https://github.com/hrzndhrn/time_zone_info)
309 | * [tzdata](https://github.com/lau/tzdata) (not recommended due to bugs)
310 |
311 | ### Based on OS-supplied zoneinfo files
312 |
313 | Recommended for embedded devices.
314 |
315 | * [zoneinfo](https://github.com/smartrent/zoneinfo)
316 |
317 | ## Acknowledgments
318 |
319 | The current state of Tz wouldn't have been possible to achieve without the work of the following contributors related to time zones:
320 |
321 | * contributors adding time zone support to Elixir ([call for proposal](https://elixirforum.com/t/14743), [initial proposal](https://github.com/elixir-lang/elixir/pull/7914), [final proposal](https://github.com/elixir-lang/elixir/pull/8383));
322 | * contributors to the [time_zone_info](https://github.com/hrzndhrn/time_zone_info) library, based on which Tz could compare its speed and drastically improve performance;
323 | * contributors to the Java `java.time` package, against which Tz is testing its output.
324 |
--------------------------------------------------------------------------------
/bench/compiler.exs:
--------------------------------------------------------------------------------
1 | Benchee.run(
2 | %{
3 | "compiler" => fn ->
4 | Code.compiler_options(ignore_module_conflict: true)
5 | Tz.Compiler.compile()
6 | Code.compiler_options(ignore_module_conflict: false)
7 | end,
8 | },
9 | memory_time: 10
10 | )
11 |
--------------------------------------------------------------------------------
/lib/mix/tasks/download.ex:
--------------------------------------------------------------------------------
1 | defmodule Mix.Tasks.Tz.Download do
2 | use Mix.Task
3 |
4 | alias Tz.IanaDataDir
5 | alias Tz.Updater
6 |
7 | @shortdoc "Downloads the IANA time zone data."
8 | def run(command_line_args) do
9 | {version, dir} = command_line_args(command_line_args)
10 |
11 | dir = dir || IanaDataDir.dir()
12 |
13 | case Updater.update_tz_database(version, dir) do
14 | :error ->
15 | Mix.raise("failed to download IANA data for version #{version}")
16 |
17 | {:ok, dir} ->
18 | Mix.shell().info(
19 | "IANA time zone data for version #{version} has been extracted into #{dir}"
20 | )
21 | end
22 | end
23 |
24 | defp command_line_args([]) do
25 | {fetch_latest_version(), nil}
26 | end
27 |
28 | defp command_line_args([version]) do
29 | {version(version), nil}
30 | end
31 |
32 | defp command_line_args([version, dir]) do
33 | unless File.exists?(dir) do
34 | Mix.raise("path #{dir} doesn't exist")
35 | end
36 |
37 | {version(version), dir}
38 | end
39 |
40 | defp command_line_args(_) do
41 | Mix.raise(
42 | "command may have two optional arguments: the tz data version and the destination directory"
43 | )
44 | end
45 |
46 | defp version("latest") do
47 | fetch_latest_version()
48 | end
49 |
50 | defp version(version) do
51 | unless Regex.match?(~r/^20[0-9]{2}[a-z]$/, version) do
52 | Mix.raise("invalid version: #{version}")
53 | end
54 |
55 | version
56 | end
57 |
58 | defp fetch_latest_version() do
59 | case Updater.fetch_latest_iana_tz_version() do
60 | {:ok, version} ->
61 | version
62 |
63 | :error ->
64 | Mix.raise("failed to read the latest version of the IANA time zone database")
65 | end
66 | end
67 | end
68 |
--------------------------------------------------------------------------------
/lib/tz.ex:
--------------------------------------------------------------------------------
1 | defmodule Tz do
2 | alias Tz.PeriodsProvider
3 |
4 | @doc """
5 | Returns the IANA time zone database version.
6 | """
7 | defdelegate iana_version(), to: PeriodsProvider
8 | end
9 |
--------------------------------------------------------------------------------
/lib/tz/compiler.ex:
--------------------------------------------------------------------------------
1 | defmodule Tz.Compiler do
2 | @moduledoc false
3 |
4 | require Logger
5 | require Tz.IanaFileParser
6 | require Tz.PeriodsBuilder
7 |
8 | alias Tz.IanaDataDir
9 | alias Tz.IanaFileParser
10 | alias Tz.PeriodsBuilder
11 |
12 | def compile() do
13 | {tzdata_dir_path, tzdata_version} = tzdata_dir_and_version()
14 |
15 | validate_tzdata_dir_has_required_files(tzdata_dir_path)
16 |
17 | {periods_and_links, ongoing_rules} =
18 | for filename <-
19 | ~w(africa antarctica asia australasia backward etcetera europe northamerica southamerica)s do
20 | {zone_records, rule_records, link_records, ongoing_rules} =
21 | IanaFileParser.parse(Path.join(tzdata_dir_path, filename))
22 |
23 | Enum.each(rule_records, fn {rule_name, rules} ->
24 | ongoing_rules_count = Enum.count(rules, &(&1.to == :max))
25 |
26 | if ongoing_rules_count > 2 do
27 | raise "unexpected case: #{ongoing_rules_count} ongoing rules for rule \"#{rule_name}\""
28 | end
29 | end)
30 |
31 | periods =
32 | for {zone_name, zone_lines} <- zone_records do
33 | periods =
34 | PeriodsBuilder.build_periods(zone_lines, rule_records)
35 | |> PeriodsBuilder.periods_to_tuples_and_reverse()
36 | |> reject_periods_before_year()
37 |
38 | if length(periods) == 0 do
39 | raise "no periods for time zone #{zone_name}"
40 | end
41 |
42 | {:periods, zone_name, periods}
43 | end
44 |
45 | links =
46 | for link <- link_records do
47 | {:link, link.link_zone_name, link.canonical_zone_name}
48 | end
49 |
50 | {periods ++ links, ongoing_rules}
51 | end
52 | |> Enum.reduce(
53 | {[], %{}},
54 | fn {periods_and_links, ongoing_rules}, {all_periods_and_links, all_ongoing_rules} ->
55 | {periods_and_links ++ all_periods_and_links,
56 | Map.merge(ongoing_rules, all_ongoing_rules)}
57 | end
58 | )
59 |
60 | compile_periods(periods_and_links, tzdata_version)
61 |
62 | compile_map_ongoing_changing_rules(ongoing_rules)
63 | end
64 |
65 | defp tzdata_dir_and_version() do
66 | IanaDataDir.maybe_copy_iana_files_to_custom_dir()
67 |
68 | cond do
69 | tzdata_dir_path = IanaDataDir.relevant_tzdata_dir_path() ->
70 | "tzdata" <> tzdata_version = Path.basename(tzdata_dir_path)
71 |
72 | {tzdata_dir_path, tzdata_version}
73 |
74 | IanaDataDir.forced_iana_version() == nil ->
75 | raise "tzdata files not found"
76 |
77 | tzdata_dir_path = IanaDataDir.latest_tzdata_dir_path() ->
78 | "tzdata" <> installed_iana_version = Path.basename(tzdata_dir_path)
79 |
80 | forced_iana_version = IanaDataDir.forced_iana_version()
81 |
82 | raise(
83 | "Missing tzdata#{forced_iana_version} files.\n" <>
84 | "1. Temporarily remove the :iana_version config\n" <>
85 | "2. Download version #{forced_iana_version} " <>
86 | "by running: mix tz.download #{forced_iana_version}\n" <>
87 | "3. Restore the :iana_version config\n" <>
88 | "4. Recompile the time zone periods " <>
89 | "by running: mix deps.compile tz --force\n" <>
90 | "5. Make sure the periods are compiled with tzdata#{forced_iana_version} " <>
91 | "by running: Tz.iana_version()"
92 | )
93 |
94 | {tzdata_dir_path, installed_iana_version}
95 |
96 | true ->
97 | raise "tzdata files not found"
98 | end
99 | end
100 |
101 | defp reject_periods_before_year(periods) do
102 | case Application.get_env(:tz, :reject_periods_before_year) do
103 | nil ->
104 | periods
105 |
106 | reject_before_year ->
107 | filtered_periods =
108 | Enum.reject(periods, fn {secs, _, _, _} ->
109 | %{year: year} = gregorian_seconds_to_naive_datetime(secs)
110 | year < reject_before_year
111 | end)
112 |
113 | if length(filtered_periods) > 0 do
114 | filtered_periods
115 | else
116 | periods
117 | end
118 | end
119 | end
120 |
121 | defp gregorian_seconds_to_naive_datetime(seconds) do
122 | :calendar.gregorian_seconds_to_datetime(seconds)
123 | |> NaiveDateTime.from_erl!()
124 | end
125 |
126 | def compile_periods(periods_and_links, tzdata_version) do
127 | quoted = [
128 | quote do
129 | @moduledoc false
130 |
131 | def iana_version() do
132 | unquote(tzdata_version)
133 | end
134 |
135 | def compiled_at() do
136 | unquote(Macro.escape(DateTime.utc_now()))
137 | end
138 |
139 | def next_period(%DateTime{} = datetime) do
140 | {gregorian_secs, _} = DateTime.to_gregorian_seconds(datetime)
141 |
142 | {:ok, periods} = periods(datetime.time_zone)
143 | reversed_periods = Enum.reverse(periods)
144 |
145 | period = Enum.find(reversed_periods, fn {from, _, _, _} -> gregorian_secs < from end)
146 |
147 | if period do
148 | period
149 | else
150 | case hd(periods) do
151 | {_, _, _, nil} ->
152 | nil
153 |
154 | {utc_secs, {utc_to_std_offset, _, _}, _, rules_and_template} ->
155 | periods =
156 | Tz.TimeZoneDatabase.generate_dynamic_periods(
157 | utc_secs,
158 | utc_to_std_offset,
159 | rules_and_template
160 | )
161 |
162 | reversed_periods = Enum.reverse(periods)
163 |
164 | Enum.find(reversed_periods, fn {from, _, _, _} -> gregorian_secs < from end)
165 | end
166 | end
167 | end
168 | end,
169 | for period_or_link <- periods_and_links do
170 | case period_or_link do
171 | {:link, link_zone_name, canonical_zone_name} ->
172 | quote do
173 | def periods(unquote(link_zone_name)) do
174 | periods(unquote(Macro.escape(canonical_zone_name)))
175 | end
176 | end
177 |
178 | {:periods, zone_name, periods} ->
179 | quote do
180 | def periods(unquote(zone_name)) do
181 | {:ok, unquote(Macro.escape(periods))}
182 | end
183 | end
184 | end
185 | end,
186 | quote do
187 | def periods(_) do
188 | {:error, :time_zone_not_found}
189 | end
190 | end
191 | ]
192 |
193 | module = :"Elixir.Tz.PeriodsProvider"
194 | Module.create(module, quoted, Macro.Env.location(__ENV__))
195 | :code.purge(module)
196 | end
197 |
198 | defp compile_map_ongoing_changing_rules(ongoing_rules) do
199 | quoted = [
200 | quote do
201 | @moduledoc false
202 | end,
203 | for {rule_name, rules} <- ongoing_rules do
204 | quote do
205 | def rules(unquote(rule_name)) do
206 | unquote(Macro.escape(rules))
207 | end
208 | end
209 | end
210 | ]
211 |
212 | module = :"Elixir.Tz.OngoingChangingRulesProvider"
213 | Module.create(module, quoted, Macro.Env.location(__ENV__))
214 | :code.purge(module)
215 | end
216 |
217 | defp validate_tzdata_dir_has_required_files(tzdata_dir_path) do
218 | required_files = [
219 | "africa",
220 | "antarctica",
221 | "asia",
222 | "australasia",
223 | "backward",
224 | "etcetera",
225 | "europe",
226 | "northamerica",
227 | "southamerica",
228 | "iso3166.tab",
229 | "zone1970.tab"
230 | ]
231 |
232 | missing_files = Enum.filter(required_files, &(!File.exists?(Path.join(tzdata_dir_path, &1))))
233 |
234 | if length(missing_files) > 0 do
235 | raise "files #{inspect(missing_files)} are missing in #{tzdata_dir_path}"
236 | end
237 | end
238 | end
239 |
--------------------------------------------------------------------------------
/lib/tz/compiler_runner.ex:
--------------------------------------------------------------------------------
1 | defmodule Tz.CompilerRunner do
2 | @moduledoc false
3 |
4 | all_env = Application.get_all_env(:tz)
5 |
6 | known_env_keys =
7 | [
8 | :http_client,
9 | :data_dir,
10 | :iana_version,
11 | :build_dst_periods_until_year,
12 | :reject_periods_before_year,
13 | Tz.HTTP.Mint.HTTPClient
14 | ]
15 |
16 | unknown_env_keys =
17 | Keyword.drop(all_env, known_env_keys)
18 | |> Keyword.keys()
19 |
20 | if unknown_env_keys != [] do
21 | joined_known_env_keys =
22 | known_env_keys
23 | |> Enum.map(&":#{to_string(&1)}")
24 | |> Enum.join(", ")
25 |
26 | raise "possible options are #{joined_known_env_keys}"
27 | end
28 |
29 | data_dir = Application.compile_env(:tz, :data_dir)
30 | forced_iana_version = Application.compile_env(:tz, :iana_version)
31 |
32 | if forced_iana_version && !Regex.match?(~r/^20[0-9]{2}[a-z]$/, forced_iana_version) do
33 | raise "the value \"#{forced_iana_version}\" provided for the :iana_version config is invalid"
34 | end
35 |
36 | if forced_iana_version && !data_dir do
37 | raise "when setting a specific IANA version to use, " <>
38 | "the files must be stored in a custom directory via the :data_dir configuration"
39 | end
40 |
41 | require Tz.Compiler
42 |
43 | Tz.Compiler.compile()
44 | end
45 |
--------------------------------------------------------------------------------
/lib/tz/http.ex:
--------------------------------------------------------------------------------
1 | defmodule Tz.HTTP do
2 | @doc """
3 | Return the http client module configured for tz.
4 | """
5 | def get_http_client!() do
6 | case Application.get_env(:tz, :http_client) do
7 | nil -> default_client!()
8 | client -> client
9 | end
10 | end
11 |
12 | defp default_client!() do
13 | if Code.ensure_loaded?(Mint.HTTP) do
14 | Tz.HTTP.Mint.HTTPClient
15 | else
16 | raise "No HTTP client found. Add :mint as a dependency, or specify a custom HTTP client by the :http_client environment variable."
17 | end
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/lib/tz/http/http_client.ex:
--------------------------------------------------------------------------------
1 | defmodule Tz.HTTP.HTTPClient do
2 | @moduledoc """
3 | A behaviour allowing to plug in any HTTP client.
4 | """
5 |
6 | @callback request(String.t(), String.t()) :: struct | nil
7 | end
8 |
--------------------------------------------------------------------------------
/lib/tz/http/http_response.ex:
--------------------------------------------------------------------------------
1 | defmodule Tz.HTTP.HTTPResponse do
2 | @moduledoc false
3 |
4 | defstruct status_code: nil, body: []
5 | end
6 |
--------------------------------------------------------------------------------
/lib/tz/http/mint/http_client.ex:
--------------------------------------------------------------------------------
1 | if Code.ensure_loaded?(Mint.HTTP) do
2 | defmodule Tz.HTTP.Mint.HTTPClient do
3 | @behaviour Tz.HTTP.HTTPClient
4 |
5 | @moduledoc false
6 |
7 | alias Mint.HTTP
8 | alias Tz.HTTP.Mint.HTTPResponse, as: MintHTTPResponse
9 | alias Tz.HTTP.HTTPResponse
10 |
11 | @impl Tz.HTTP.HTTPClient
12 | def request(hostname, path) do
13 | opts = Application.get_env(:tz, Tz.HTTP.Mint.HTTPClient, [])
14 |
15 | with {:ok, conn} <- HTTP.connect(:https, hostname, 443, opts),
16 | {:ok, conn, _} <- HTTP.request(conn, "GET", path, [], nil),
17 | {:ok, response = %MintHTTPResponse{}} <- recv_response(conn) do
18 | {:ok, _conn} = HTTP.close(conn)
19 |
20 | %HTTPResponse{
21 | status_code: response.status_code,
22 | body: response.body
23 | }
24 | end
25 | end
26 |
27 | defp recv_response(conn, http_response \\ %MintHTTPResponse{}) do
28 | receive do
29 | message ->
30 | with {:ok, conn, mint_messages} <- HTTP.stream(conn, message) do
31 | case MintHTTPResponse.parse(mint_messages, http_response) do
32 | {:ok, http_response = %MintHTTPResponse{complete?: true}} ->
33 | {:ok, http_response}
34 |
35 | {:ok, http_response} ->
36 | recv_response(conn, http_response)
37 | end
38 | end
39 | end
40 | end
41 | end
42 | end
43 |
--------------------------------------------------------------------------------
/lib/tz/http/mint/http_response.ex:
--------------------------------------------------------------------------------
1 | if Code.ensure_loaded?(Mint.HTTP) do
2 | defmodule Tz.HTTP.Mint.HTTPResponse do
3 | @moduledoc false
4 |
5 | defstruct status_code: nil, headers: nil, body: [], complete?: false
6 |
7 | def parse([{:status, _, status_code} | mint_messages], %__MODULE__{} = http_response) do
8 | parse(mint_messages, %{http_response | status_code: status_code})
9 | end
10 |
11 | def parse([{:headers, _, headers} | mint_messages], %__MODULE__{} = http_response) do
12 | parse(mint_messages, %{http_response | headers: headers})
13 | end
14 |
15 | def parse([{:data, _, data} | mint_messages], %__MODULE__{} = http_response) do
16 | parse(mint_messages, %{http_response | body: [data | http_response.body]})
17 | end
18 |
19 | def parse([{:done, _}], %__MODULE__{} = http_response) do
20 | body =
21 | http_response.body
22 | |> Enum.reverse()
23 | |> Enum.join()
24 |
25 | {:ok, %{http_response | body: body, complete?: true}}
26 | end
27 |
28 | def parse([], http_response), do: {:ok, http_response}
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/lib/tz/iana_data_dir.ex:
--------------------------------------------------------------------------------
1 | defmodule Tz.IanaDataDir do
2 | @moduledoc false
3 |
4 | require Logger
5 |
6 | def dir(), do: Application.get_env(:tz, :data_dir) || to_string(:code.priv_dir(:tz))
7 |
8 | def forced_iana_version(), do: Application.get_env(:tz, :iana_version)
9 |
10 | defp latest_dir_name([]), do: nil
11 |
12 | defp latest_dir_name(dir_names) do
13 | Enum.max(dir_names)
14 | end
15 |
16 | defp relevant_dir_name([]), do: nil
17 |
18 | defp relevant_dir_name(dir_names) do
19 | if forced_version = forced_iana_version() do
20 | Enum.find(dir_names, &(&1 == "tzdata#{forced_version}"))
21 | else
22 | latest_dir_name(dir_names)
23 | end
24 | end
25 |
26 | defp list_dir_names(parent_dir) do
27 | File.ls!(parent_dir)
28 | |> Enum.filter(&Regex.match?(~r/^tzdata20[0-9]{2}[a-z]$/, &1))
29 | end
30 |
31 | def latest_tzdata_dir_name() do
32 | latest_dir_name(list_dir_names(dir()))
33 | end
34 |
35 | def relevant_tzdata_dir_name() do
36 | relevant_dir_name(list_dir_names(dir()))
37 | end
38 |
39 | def latest_tzdata_dir_path() do
40 | if dir_name = latest_dir_name(list_dir_names(dir())) do
41 | Path.join(dir(), dir_name)
42 | end
43 | end
44 |
45 | def relevant_tzdata_dir_path() do
46 | if dir_name = relevant_dir_name(list_dir_names(dir())) do
47 | Path.join(dir(), dir_name)
48 | end
49 | end
50 |
51 | def latest_tzdata_version() do
52 | if dir_name = latest_dir_name(list_dir_names(dir())) do
53 | "tzdata" <> version = dir_name
54 | version
55 | end
56 | end
57 |
58 | def relevant_tzdata_version() do
59 | if dir_name = relevant_dir_name(list_dir_names(dir())) do
60 | "tzdata" <> version = dir_name
61 | version
62 | end
63 | end
64 |
65 | def maybe_copy_iana_files_to_custom_dir() do
66 | cond do
67 | # no custom dir
68 | to_string(:code.priv_dir(:tz)) == dir() ->
69 | nil
70 |
71 | # tzdata dir already exists
72 | relevant_tzdata_dir_path() ->
73 | nil
74 |
75 | true ->
76 | lib_dir_names = list_dir_names(to_string(:code.priv_dir(:tz)))
77 |
78 | cond do
79 | lib_dir_name = relevant_dir_name(lib_dir_names) ->
80 | File.cp_r!(
81 | Path.join(:code.priv_dir(:tz), lib_dir_name),
82 | Path.join(dir(), lib_dir_name)
83 | )
84 |
85 | Logger.info(
86 | "Moved #{Path.join(:code.priv_dir(:tz), lib_dir_name)} to #{Path.join(dir(), lib_dir_name)}"
87 | )
88 |
89 | lib_dir_name = latest_dir_name(lib_dir_names) ->
90 | app_dir_name = latest_tzdata_dir_name()
91 |
92 | if !app_dir_name || app_dir_name < lib_dir_name do
93 | File.cp_r!(
94 | Path.join(:code.priv_dir(:tz), lib_dir_name),
95 | Path.join(dir(), lib_dir_name)
96 | )
97 |
98 | Logger.info(
99 | "Moved #{Path.join(:code.priv_dir(:tz), lib_dir_name)} to #{Path.join(dir(), lib_dir_name)}"
100 | )
101 | end
102 |
103 | true ->
104 | raise "tzdata files not found"
105 | end
106 | end
107 | end
108 |
109 | def extract_tzdata_into_dir(version, content, dir \\ dir()) do
110 | tmp_archive_path = Path.join(dir, "tzdata#{version}.tar.gz")
111 | tzdata_dir_name = "tzdata#{version}"
112 | :ok = File.write!(tmp_archive_path, content)
113 |
114 | files_to_extract = [
115 | ~c"africa",
116 | ~c"antarctica",
117 | ~c"asia",
118 | ~c"australasia",
119 | ~c"backward",
120 | ~c"etcetera",
121 | ~c"europe",
122 | ~c"northamerica",
123 | ~c"southamerica",
124 | ~c"iso3166.tab",
125 | ~c"zone1970.tab"
126 | ]
127 |
128 | :ok =
129 | :erl_tar.extract(tmp_archive_path, [
130 | :compressed,
131 | {:cwd, Path.join(dir, tzdata_dir_name) |> String.to_charlist()},
132 | {:files, files_to_extract}
133 | ])
134 |
135 | :ok = File.rm!(tmp_archive_path)
136 |
137 | Path.join(dir, tzdata_dir_name)
138 | end
139 |
140 | def delete_tzdata_dir(version) do
141 | if version != forced_iana_version() do
142 | Path.join(dir(), "tzdata#{version}")
143 | |> File.rm_rf!()
144 | end
145 | end
146 | end
147 |
--------------------------------------------------------------------------------
/lib/tz/iana_file_parser.ex:
--------------------------------------------------------------------------------
1 | defmodule Tz.IanaFileParser do
2 | @moduledoc false
3 | # https://data.iana.org/time-zones/tzdb/tz-how-to.html
4 |
5 | @build_dst_periods_until_year Application.compile_env(
6 | :tz,
7 | :build_dst_periods_until_year,
8 | 5 + NaiveDateTime.utc_now().year
9 | )
10 |
11 | def parse(file_path) do
12 | records =
13 | File.stream!(file_path)
14 | |> strip_comments()
15 | |> strip_empty()
16 | |> trim()
17 | |> Enum.to_list()
18 | |> parse_strings_into_maps()
19 |
20 | zones = Enum.filter(records, &(&1.record_type == :zone))
21 | rules = Enum.filter(records, &(&1.record_type == :rule))
22 | links = Enum.filter(records, &(&1.record_type == :link))
23 |
24 | {
25 | denormalized_zone_data(zones),
26 | denormalized_rule_data(rules, @build_dst_periods_until_year),
27 | links,
28 | Enum.filter(rules, & &1.ongoing_switch)
29 | |> Enum.group_by(& &1.name)
30 | }
31 | end
32 |
33 | defp strip_comments(stream) do
34 | stream
35 | |> Stream.filter(&(!Regex.match?(~r/^[\s]*#/, &1)))
36 | |> Stream.map(&Regex.replace(~r/[\s]*#.+/, &1, ""))
37 | end
38 |
39 | defp strip_empty(stream) do
40 | Stream.filter(stream, &(!Regex.match?(~r/^[\s]*\n$/, &1)))
41 | end
42 |
43 | defp trim(stream) do
44 | Stream.map(stream, &String.trim(&1))
45 | end
46 |
47 | defp parse_strings_into_maps(strings, state \\ %{current_zone_name: nil})
48 |
49 | defp parse_strings_into_maps([], _), do: []
50 |
51 | defp parse_strings_into_maps([string | tail], state) do
52 | %{current_zone_name: current_zone_name} = state
53 |
54 | {maps, state} =
55 | cond do
56 | String.starts_with?(string, "Rule") ->
57 | {parse_rule_string_into_maps(string), %{current_zone_name: nil}}
58 |
59 | String.starts_with?(string, "Link") ->
60 | {[parse_link_string_into_map(string)], %{current_zone_name: nil}}
61 |
62 | String.starts_with?(string, "Zone") ->
63 | zone = parse_zone_string_into_map(string)
64 | {[zone], %{current_zone_name: zone.name}}
65 |
66 | true ->
67 | zone = parse_zone_string_into_map(string, current_zone_name)
68 | {[zone], %{current_zone_name: current_zone_name}}
69 | end
70 |
71 | maps ++ parse_strings_into_maps(tail, state)
72 | end
73 |
74 | defp parse_rule_string_into_maps(rule_string) do
75 | Enum.zip([
76 | [
77 | :name,
78 | :from_year,
79 | :to_year,
80 | :_,
81 | :month,
82 | :day,
83 | :time,
84 | :local_offset_from_std_time,
85 | :letter
86 | ],
87 | tl(String.split(rule_string, ~r{\s}, trim: true, parts: 10))
88 | |> Enum.map(&String.trim(&1))
89 | ])
90 | |> Enum.into(%{})
91 | |> Map.delete(:_)
92 | |> Map.put(:record_type, :rule)
93 | |> transform_rule()
94 | end
95 |
96 | defp parse_link_string_into_map(link_string) do
97 | Enum.zip([
98 | [:canonical_zone_name, :link_zone_name],
99 | tl(String.split(link_string, ~r{\s}, trim: true, parts: 3))
100 | |> Enum.map(&String.trim(&1))
101 | ])
102 | |> Enum.into(%{})
103 | |> Map.put(:record_type, :link)
104 | end
105 |
106 | defp parse_zone_string_into_map(zone_string) do
107 | Enum.zip([
108 | [:name, :std_offset_from_utc_time, :rules, :format_time_zone_abbr, :to],
109 | tl(String.split(zone_string, ~r{\s}, trim: true, parts: 6))
110 | |> Enum.map(&String.trim(&1))
111 | ])
112 | |> Enum.into(%{})
113 | |> Map.merge(%{to: :max}, fn _k, v1, _v2 -> v1 end)
114 | |> Map.put(:record_type, :zone)
115 | |> transform_zone()
116 | end
117 |
118 | defp parse_zone_string_into_map(zone_string, current_zone_name) do
119 | Enum.zip([
120 | [:name, :std_offset_from_utc_time, :rules, :format_time_zone_abbr, :to],
121 | [
122 | current_zone_name
123 | | String.split(zone_string, ~r{\s}, trim: true, parts: 4)
124 | |> Enum.map(&String.trim(&1))
125 | ]
126 | ])
127 | |> Enum.into(%{})
128 | |> Map.merge(%{to: :max}, fn _k, v1, _v2 -> v1 end)
129 | |> Map.put(:record_type, :zone)
130 | |> transform_zone()
131 | end
132 |
133 | defp transform_zone(%{} = zone) do
134 | rules = String.trim(zone.rules)
135 |
136 | rules =
137 | cond do
138 | rules == "-" ->
139 | 0
140 |
141 | String.match?(rules, ~r/[+-]?[0-9]+/) ->
142 | offset_string_to_seconds(rules)
143 |
144 | rules ->
145 | rules
146 | end
147 |
148 | std_offset = offset_string_to_seconds(zone.std_offset_from_utc_time)
149 |
150 | %{
151 | record_type: :zone,
152 | name: zone.name,
153 | rules: rules,
154 | to: parse_to_field_string(zone.to),
155 | std_offset_from_utc_time: std_offset,
156 | format_time_zone_abbr: zone.format_time_zone_abbr
157 | }
158 | end
159 |
160 | def transform_rule(%{} = rule) do
161 | {from_year, to_year} = year_strings_to_integers(rule.from_year, rule.to_year)
162 | month = month_string_to_integer(rule.month)
163 | {hour, minute, second, time_modifier} = parse_time_string(rule.time)
164 |
165 | {ongoing_switch, to_year} =
166 | if to_year == :max do
167 | {true, from_year}
168 | else
169 | {false, to_year}
170 | end
171 |
172 | for year <- Range.new(from_year, to_year) do
173 | parsed_day = parse_day_string(rule.day)
174 | {year, month, day} = parsed_day_to_date(year, month, parsed_day)
175 |
176 | naive_date_time = new_naive_date_time(year, month, day, hour, minute, second)
177 |
178 | local_offset = offset_string_to_seconds(rule.local_offset_from_std_time)
179 |
180 | %{
181 | record_type: :rule,
182 | from: {naive_date_time, time_modifier},
183 | ongoing_switch: ongoing_switch,
184 | name: rule.name,
185 | local_offset_from_std_time: local_offset,
186 | letter: if(rule.letter == "-", do: "", else: rule.letter),
187 | __datetime_data: %{
188 | date: {year, month, parsed_day},
189 | time: {hour, minute, second, time_modifier}
190 | }
191 | }
192 | end
193 | end
194 |
195 | def change_rule_year(rule, year, ongoing_switch \\ false)
196 |
197 | def change_rule_year(%{to: _} = rule, year, ongoing_switch) do
198 | rule
199 | |> Map.put(:ongoing_switch, ongoing_switch)
200 | |> Map.delete(:to)
201 | |> change_rule_year(year, ongoing_switch)
202 | end
203 |
204 | def change_rule_year(%{} = rule, year, ongoing_switch) do
205 | %{
206 | date: {_, month, parsed_day},
207 | time: {hour, minute, second, time_modifier}
208 | } = rule.__datetime_data
209 |
210 | {year, month, day} = parsed_day_to_date(year, month, parsed_day)
211 | naive_date_time = new_naive_date_time(year, month, day, hour, minute, second)
212 |
213 | %{rule | from: {naive_date_time, time_modifier}, ongoing_switch: ongoing_switch}
214 | end
215 |
216 | defp new_naive_date_time(year, month, day, 24, minute, second) do
217 | {:ok, naive_date_time} = NaiveDateTime.new(year, month, day, 0, minute, second)
218 | NaiveDateTime.add(naive_date_time, 86400)
219 | end
220 |
221 | defp new_naive_date_time(year, month, day, 25, minute, second) do
222 | {:ok, naive_date_time} = NaiveDateTime.new(year, month, day, 1, minute, second)
223 | NaiveDateTime.add(naive_date_time, 86400)
224 | end
225 |
226 | defp new_naive_date_time(year, month, day, hour, minute, second) do
227 | {:ok, naive_date_time} = NaiveDateTime.new(year, month, day, hour, minute, second)
228 | naive_date_time
229 | end
230 |
231 | defp parse_day_string(day_string) do
232 | cond do
233 | String.contains?(day_string, "last") ->
234 | "last" <> day_of_week_string = day_string
235 | day_of_week = day_of_week_string_to_integer(day_of_week_string)
236 | {:last_dow, day_of_week}
237 |
238 | String.contains?(day_string, "<=") ->
239 | [day_of_week_string, on_or_before_day] = String.split(day_string, "<=", trim: true)
240 | day_of_week = day_of_week_string_to_integer(day_of_week_string)
241 | on_or_before_day = String.to_integer(on_or_before_day)
242 | {:dow_equal_or_before_day, day_of_week, on_or_before_day}
243 |
244 | String.contains?(day_string, ">=") ->
245 | [day_of_week_string, on_or_after_day] = String.split(day_string, ">=", trim: true)
246 | day_of_week = day_of_week_string_to_integer(day_of_week_string)
247 | on_or_after_day = String.to_integer(on_or_after_day)
248 | {:dow_equal_or_after_day, day_of_week, on_or_after_day}
249 |
250 | String.match?(day_string, ~r/[0-9]+/) ->
251 | {:day, String.to_integer(day_string)}
252 |
253 | true ->
254 | raise "could not parse day from rule (day to parse is \"#{day_string}\")"
255 | end
256 | end
257 |
258 | defp parsed_day_to_date(year, month, parsed_day) do
259 | case parsed_day do
260 | {:last_dow, day_of_week} ->
261 | day = day_at_last_given_day_of_week_of_month(year, month, day_of_week)
262 | {year, month, day}
263 |
264 | {:dow_equal_or_before_day, day_of_week, on_or_before_day} ->
265 | day_at_given_day_of_week_of_month(
266 | year,
267 | month,
268 | day_of_week,
269 | :on_or_before_day,
270 | on_or_before_day
271 | )
272 |
273 | {:dow_equal_or_after_day, day_of_week, on_or_after_day} ->
274 | day_at_given_day_of_week_of_month(
275 | year,
276 | month,
277 | day_of_week,
278 | :on_or_after_day,
279 | on_or_after_day
280 | )
281 |
282 | {:day, day} ->
283 | {year, month, day}
284 | end
285 | end
286 |
287 | defp parse_to_field_string(:min), do: :min
288 | defp parse_to_field_string(:max), do: :max
289 |
290 | defp parse_to_field_string(to_field_string) do
291 | {year, month, day, hour, minute, second, time_modifier} =
292 | case String.split(to_field_string) do
293 | [year, month, day, time] ->
294 | year = String.to_integer(year)
295 | month = month_string_to_integer(month)
296 | parsed_day = parse_day_string(day)
297 | {year, month, day} = parsed_day_to_date(year, month, parsed_day)
298 |
299 | {hour, minute, second, time_modifier} = parse_time_string(time)
300 | {year, month, day, hour, minute, second, time_modifier}
301 |
302 | [year, month, day] ->
303 | year = String.to_integer(year)
304 | month = month_string_to_integer(month)
305 | parsed_day = parse_day_string(day)
306 | {year, month, day} = parsed_day_to_date(year, month, parsed_day)
307 |
308 | {year, month, day, 0, 0, 0, :wall}
309 |
310 | [year, month] ->
311 | year = String.to_integer(year)
312 | month = month_string_to_integer(month)
313 |
314 | {year, month, 1, 0, 0, 0, :wall}
315 |
316 | [year] ->
317 | year = String.to_integer(year)
318 |
319 | {year, 1, 1, 0, 0, 0, :wall}
320 | end
321 |
322 | naive_date_time = new_naive_date_time(year, month, day, hour, minute, second)
323 | {naive_date_time, time_modifier}
324 | end
325 |
326 | defp day_at_given_day_of_week_of_month(
327 | year,
328 | month,
329 | day_of_week,
330 | :on_or_before_day,
331 | on_or_before_day
332 | ) do
333 | {:ok, on_or_before_date} = Date.new(year, month, on_or_before_day)
334 |
335 | days_to_subtract = diff_days_of_week(day_of_week, Date.day_of_week(on_or_before_date))
336 | date = Date.add(on_or_before_date, -1 * days_to_subtract)
337 |
338 | {date.year, date.month, date.day}
339 | end
340 |
341 | defp day_at_given_day_of_week_of_month(
342 | year,
343 | month,
344 | day_of_week,
345 | :on_or_after_day,
346 | on_or_after_day
347 | ) do
348 | {:ok, on_or_after_date} = Date.new(year, month, on_or_after_day)
349 |
350 | days_to_add = diff_days_of_week(Date.day_of_week(on_or_after_date), day_of_week)
351 | date = Date.add(on_or_after_date, days_to_add)
352 |
353 | {date.year, date.month, date.day}
354 | end
355 |
356 | defp day_at_last_given_day_of_week_of_month(year, month, day_of_week) do
357 | date_at_end_of_month = date_at_end_of_month(year, month)
358 | days_to_subtract = diff_days_of_week(day_of_week, Date.day_of_week(date_at_end_of_month))
359 | date = Date.add(date_at_end_of_month, -1 * days_to_subtract)
360 | date.day
361 | end
362 |
363 | defp diff_days_of_week(from_day_of_week, to_day_of_week) do
364 | rem(7 + (to_day_of_week - from_day_of_week), 7)
365 | end
366 |
367 | defp date_at_end_of_month(year, month) do
368 | {:ok, date} = Date.new(year, month, 1)
369 | last_day = Date.days_in_month(date)
370 | {:ok, date} = Date.new(year, month, last_day)
371 | date
372 | end
373 |
374 | defp year_strings_to_integers(from_year, "only") do
375 | {String.to_integer(from_year), String.to_integer(from_year)}
376 | end
377 |
378 | defp year_strings_to_integers(from_year, "max") do
379 | {String.to_integer(from_year), :max}
380 | end
381 |
382 | defp year_strings_to_integers(from_year, to_year) do
383 | {String.to_integer(from_year), String.to_integer(to_year)}
384 | end
385 |
386 | defp month_string_to_integer(month_string) do
387 | month_names = [
388 | {"Jan", "January"},
389 | {"Feb", "February"},
390 | {"Mar", "March"},
391 | {"Apr", "April"},
392 | {"May", "May"},
393 | {"Jun", "June"},
394 | {"Jul", "July"},
395 | {"Aug", "August"},
396 | {"Sep", "September"},
397 | {"Oct", "October"},
398 | {"Nov", "November"},
399 | {"Dec", "December"}
400 | ]
401 |
402 | index =
403 | Enum.find_index(month_names, fn {abbr, full} ->
404 | month_string == abbr || month_string == full
405 | end)
406 |
407 | 1 + index
408 | end
409 |
410 | defp day_of_week_string_to_integer(day_of_week_string) do
411 | day_of_week_names = [
412 | {"Mon", "Monday"},
413 | {"Tue", "Tuesday"},
414 | {"Wed", "Wednesday"},
415 | {"Thu", "Thursday"},
416 | {"Fri", "Friday"},
417 | {"Sat", "Saturday"},
418 | {"Sun", "Sunday"}
419 | ]
420 |
421 | index =
422 | Enum.find_index(day_of_week_names, fn {abbr, full} ->
423 | day_of_week_string == abbr || day_of_week_string == full
424 | end)
425 |
426 | 1 + index
427 | end
428 |
429 | defp parse_time_string(time_string) do
430 | {hour, minute, second, time_modifier} =
431 | String.split(time_string, ~r{[:gsuz]}, include_captures: true, trim: true)
432 | |> case do
433 | [hour, ":", minute, ":", second, time_modifier] when time_modifier in ["g", "u", "z"] ->
434 | {hour, minute, second, :utc}
435 |
436 | [hour, ":", minute, time_modifier] when time_modifier in ["g", "u", "z"] ->
437 | {hour, minute, "0", :utc}
438 |
439 | [hour, ":", minute, ":", second, time_modifier] when time_modifier == "s" ->
440 | {hour, minute, second, :standard}
441 |
442 | [hour, ":", minute, time_modifier] when time_modifier == "s" ->
443 | {hour, minute, "0", :standard}
444 |
445 | [hour, ":", minute, ":", second] ->
446 | {hour, minute, second, :wall}
447 |
448 | [hour, ":", minute] ->
449 | {hour, minute, "0", :wall}
450 |
451 | _ ->
452 | raise "could not parse time string \"#{time_string}\""
453 | end
454 |
455 | hour = String.to_integer(hour)
456 | minute = String.to_integer(minute)
457 | second = String.to_integer(second)
458 |
459 | {hour, minute, second, time_modifier}
460 | end
461 |
462 | defp offset_string_to_seconds(offset_string) do
463 | {is_negative, hours, minutes, seconds} =
464 | String.split(offset_string, ~r{[:\-]}, include_captures: true, trim: true)
465 | |> case do
466 | ["-", hours, ":", minutes, ":", seconds] ->
467 | {true, hours, minutes, seconds}
468 |
469 | ["-", hours, ":", minutes] ->
470 | {true, hours, minutes, "0"}
471 |
472 | [hours, ":", minutes, ":", seconds] ->
473 | {false, hours, minutes, seconds}
474 |
475 | [hours, ":", minutes] ->
476 | {false, hours, minutes, "0"}
477 |
478 | ["-", hours] ->
479 | {true, hours, "0", "0"}
480 |
481 | [hours] ->
482 | {false, hours, "0", "0"}
483 |
484 | _ ->
485 | raise "could not parse time string \"#{offset_string}\""
486 | end
487 |
488 | hours = String.to_integer(hours)
489 | minutes = String.to_integer(minutes)
490 | seconds = String.to_integer(seconds)
491 |
492 | total_seconds = hours * 3600 + minutes * 60 + seconds
493 |
494 | if(is_negative, do: -1 * total_seconds, else: total_seconds)
495 | end
496 |
497 | defp denormalized_zone_data(zone_records) do
498 | zone_records
499 | |> Enum.group_by(& &1.name)
500 | |> (fn zones_by_name ->
501 | Enum.map(zones_by_name, fn {zone_name, zone_lines} ->
502 | {zone_name, denormalize_zone_lines(zone_lines)}
503 | end)
504 | |> Enum.into(%{})
505 | end).()
506 | end
507 |
508 | def denormalized_rule_data(rule_records, build_dst_periods_until_year \\ 0) do
509 | rule_records
510 | |> Enum.group_by(& &1.name)
511 | |> (fn rules_by_name ->
512 | rules_by_name
513 | |> Enum.map(fn {rule_name, rules} ->
514 | {rule_name, denormalize_rules(rules, build_dst_periods_until_year)}
515 | end)
516 | |> Enum.into(%{})
517 | end).()
518 | end
519 |
520 | defp denormalize_zone_lines(zone_lines) do
521 | zone_lines
522 | |> Enum.with_index()
523 | |> Enum.map(fn {zone_line, index} ->
524 | Map.put(
525 | zone_line,
526 | :from,
527 | cond do
528 | index == 0 -> :min
529 | true -> Enum.at(zone_lines, index - 1).to
530 | end
531 | )
532 | end)
533 | end
534 |
535 | defp denormalize_rules(rules, build_dst_periods_until_year) do
536 | ongoing_switch_rules = Enum.filter(rules, & &1.ongoing_switch)
537 |
538 | rules =
539 | case ongoing_switch_rules do
540 | [] ->
541 | rules
542 |
543 | [rule1, rule2] ->
544 | last_year =
545 | Enum.max([
546 | build_dst_periods_until_year,
547 | elem(List.last(rules).from, 0).year,
548 | elem(rule1.from, 0).year,
549 | elem(rule2.from, 0).year
550 | ])
551 |
552 | Enum.filter(rules, &(!&1.ongoing_switch)) ++
553 | for year <- Range.new(elem(rule1.from, 0).year, last_year) do
554 | change_rule_year(rule1, year)
555 | end ++
556 | [change_rule_year(rule1, last_year + 1, true)] ++
557 | for year <- Range.new(elem(rule2.from, 0).year, last_year) do
558 | change_rule_year(rule2, year)
559 | end ++
560 | [change_rule_year(rule2, last_year + 1, true)]
561 |
562 | _ ->
563 | raise "unexpected number of rules to \"max\", rules: \"#{inspect(rules)}\""
564 | end
565 |
566 | rules =
567 | Enum.sort(rules, fn rule1, rule2 ->
568 | naive_date_time1 = elem(rule1.from, 0)
569 | time_modifier1 = elem(rule1.from, 1)
570 | naive_date_time2 = elem(rule2.from, 0)
571 | time_modifier2 = elem(rule2.from, 1)
572 |
573 | diff = NaiveDateTime.diff(naive_date_time1, naive_date_time2)
574 |
575 | if abs(diff) < 86400 && time_modifier1 != time_modifier2 do
576 | raise "unexpected case"
577 | end
578 |
579 | diff < 0
580 | end)
581 |
582 | rules
583 | |> Enum.with_index()
584 | |> Enum.map(fn {rule, index} ->
585 | rule
586 | |> Map.put(
587 | :to,
588 | if rule.ongoing_switch do
589 | :max
590 | else
591 | case Enum.at(rules, index + 1, nil) do
592 | nil -> :max
593 | next_rule -> next_rule.from
594 | end
595 | end
596 | )
597 | |> Map.delete(:ongoing_switch)
598 | end)
599 | end
600 | end
601 |
--------------------------------------------------------------------------------
/lib/tz/periods_builder.ex:
--------------------------------------------------------------------------------
1 | defmodule Tz.PeriodsBuilder do
2 | @moduledoc false
3 |
4 | def build_periods(
5 | zone_lines,
6 | rule_records,
7 | mode \\ :compilation,
8 | prev_period \\ nil,
9 | periods \\ []
10 | )
11 |
12 | def build_periods([], _rule_records, _mode, _prev_period, periods), do: Enum.reverse(periods)
13 |
14 | def build_periods([zone_line | rest_zone_lines], rule_records, mode, prev_period, periods) do
15 | rules = Map.get(rule_records, zone_line.rules, zone_line.rules)
16 |
17 | periods =
18 | build_periods_for_zone_line(zone_line, rules, mode, prev_period)
19 | |> concat_dedup_periods(periods)
20 |
21 | build_periods(rest_zone_lines, rule_records, mode, hd(periods), periods)
22 | end
23 |
24 | defp concat_dedup_periods(periods, []), do: periods
25 |
26 | defp concat_dedup_periods(periods1, [first_period2 | tail_period2] = periods2) do
27 | last_period1 = List.last(periods1)
28 | compare_keys = [:std_offset_from_utc_time, :local_offset_from_std_time, :zone_abbr]
29 |
30 | cond do
31 | Map.take(last_period1, compare_keys) == Map.take(first_period2, compare_keys) ->
32 | (periods1 |> Enum.reverse() |> tl() |> Enum.reverse()) ++
33 | [%{first_period2 | to: last_period1.to} | tail_period2]
34 |
35 | true ->
36 | periods1 ++ periods2
37 | end
38 | end
39 |
40 | defp offset_diff_from_prev_period(_zone_line, _local_offset, nil), do: 0
41 |
42 | defp offset_diff_from_prev_period(zone_line, local_offset, prev_period) do
43 | total_offset = zone_line.std_offset_from_utc_time + local_offset
44 |
45 | prev_total_offset =
46 | prev_period.std_offset_from_utc_time + prev_period.local_offset_from_std_time
47 |
48 | total_offset - prev_total_offset
49 | end
50 |
51 | defp build_periods_for_zone_line(zone_line, offset, _mode, prev_period)
52 | when is_integer(offset) do
53 | if zone_line.from != :min && prev_period != nil do
54 | {zone_from, zone_from_modifier} = zone_line.from
55 |
56 | if prev_period.to[zone_from_modifier] != zone_from do
57 | raise "logic error"
58 | end
59 | end
60 |
61 | offset_diff = offset_diff_from_prev_period(zone_line, offset, prev_period)
62 |
63 | period_from =
64 | if zone_line.from == :min do
65 | :min
66 | else
67 | add_to_and_convert_date_tuple(
68 | {prev_period.to.wall, :wall},
69 | offset_diff,
70 | zone_line.std_offset_from_utc_time,
71 | offset
72 | )
73 | end
74 |
75 | [
76 | %{
77 | from: period_from,
78 | to: convert_date_tuple(zone_line.to, zone_line.std_offset_from_utc_time, offset),
79 | std_offset_from_utc_time: zone_line.std_offset_from_utc_time,
80 | local_offset_from_std_time: offset,
81 | zone_abbr: zone_abbr(zone_line, offset)
82 | }
83 | ]
84 | end
85 |
86 | defp build_periods_for_zone_line(zone_line, rules, mode, prev_period) when is_list(rules) do
87 | if zone_line.from != :min && prev_period != nil do
88 | {zone_from, zone_from_modifier} = zone_line.from
89 |
90 | if prev_period.to[zone_from_modifier] != zone_from do
91 | raise "logic error"
92 | end
93 | end
94 |
95 | if mode == :dynamic_far_future do
96 | rules
97 | else
98 | rules
99 | |> filter_rules_for_zone_line(
100 | zone_line,
101 | prev_period,
102 | if(prev_period == nil, do: 0, else: prev_period.local_offset_from_std_time)
103 | )
104 | |> maybe_pad_left_rule(zone_line, prev_period)
105 | |> trim_zone_rules(zone_line, prev_period)
106 | end
107 | |> do_build_periods_for_zone_line(zone_line, prev_period, [])
108 | end
109 |
110 | defp filter_rules_for_zone_line(
111 | rules,
112 | zone_line,
113 | prev_period,
114 | prev_local_offset_from_std_time,
115 | filtered_rules \\ []
116 | )
117 |
118 | defp filter_rules_for_zone_line(rules, %{from: :min, to: :max}, _, _, _), do: rules
119 |
120 | defp filter_rules_for_zone_line([], _zone_line, _, _, filtered_rules),
121 | do: Enum.reverse(filtered_rules)
122 |
123 | defp filter_rules_for_zone_line(
124 | [rule | rest_rules],
125 | zone_line,
126 | prev_period,
127 | prev_local_offset_from_std_time,
128 | filtered_rules
129 | ) do
130 | is_rule_included =
131 | cond do
132 | zone_line.to == :max && rule.to == :max ->
133 | true
134 |
135 | zone_line.to == :max ->
136 | {rule_to, rule_to_modifier} = rule.to
137 |
138 | prev_period == nil ||
139 | NaiveDateTime.compare(prev_period.to[rule_to_modifier], rule_to) == :lt
140 |
141 | zone_line.from == :min || rule.to == :max ->
142 | {zone_to, zone_to_modifier} = zone_line.to
143 |
144 | rule_from =
145 | convert_date_tuple(
146 | rule.from,
147 | prev_period.std_offset_from_utc_time,
148 | prev_local_offset_from_std_time
149 | )
150 |
151 | NaiveDateTime.compare(zone_to, rule_from[zone_to_modifier]) == :gt
152 |
153 | true ->
154 | {zone_to, zone_to_modifier} = zone_line.to
155 | {rule_to, rule_to_modifier} = rule.to
156 |
157 | rule_from =
158 | convert_date_tuple(
159 | rule.from,
160 | prev_period.std_offset_from_utc_time,
161 | prev_local_offset_from_std_time
162 | )
163 |
164 | NaiveDateTime.compare(prev_period.to[rule_to_modifier], rule_to) == :lt &&
165 | NaiveDateTime.compare(zone_to, rule_from[zone_to_modifier]) == :gt
166 | end
167 |
168 | if is_rule_included do
169 | filter_rules_for_zone_line(
170 | rest_rules,
171 | zone_line,
172 | prev_period,
173 | rule.local_offset_from_std_time,
174 | [rule | filtered_rules]
175 | )
176 | else
177 | filter_rules_for_zone_line(
178 | rest_rules,
179 | zone_line,
180 | prev_period,
181 | prev_local_offset_from_std_time,
182 | filtered_rules
183 | )
184 | end
185 | end
186 |
187 | defp trim_zone_rules([], _zone_line, _), do: []
188 |
189 | defp trim_zone_rules([first_rule | tail_rules] = rules, zone_line, prev_period) do
190 | rules =
191 | if rule_starts_before_zone_line_range?(
192 | zone_line,
193 | first_rule,
194 | if(prev_period == nil, do: 0, else: prev_period.local_offset_from_std_time)
195 | ) do
196 | [%{first_rule | from: zone_line.from} | tail_rules]
197 | else
198 | rules
199 | end
200 |
201 | last_rule = List.last(rules)
202 |
203 | if rule_ends_after_zone_line_range?(zone_line, last_rule) do
204 | [%{last_rule | to: zone_line.to} | Enum.reverse(rules) |> tl()]
205 | |> Enum.reverse()
206 | else
207 | rules
208 | end
209 | end
210 |
211 | defp rule_starts_before_zone_line_range?(%{from: :min}, _rule, _), do: false
212 |
213 | defp rule_starts_before_zone_line_range?(zone_line, rule, prev_local_offset_from_std_time) do
214 | rule_from =
215 | convert_date_tuple(
216 | rule.from,
217 | zone_line.std_offset_from_utc_time,
218 | prev_local_offset_from_std_time
219 | )
220 |
221 | %{from: {zone_from, zone_from_modifier}} = zone_line
222 | NaiveDateTime.compare(rule_from[zone_from_modifier], zone_from) == :lt
223 | end
224 |
225 | defp rule_ends_after_zone_line_range?(%{to: :max}, _rule), do: false
226 | defp rule_ends_after_zone_line_range?(_zone_line, %{to: :max}), do: true
227 |
228 | defp rule_ends_after_zone_line_range?(zone_line, rule) do
229 | rule_to =
230 | convert_date_tuple(
231 | rule.to,
232 | zone_line.std_offset_from_utc_time,
233 | rule.local_offset_from_std_time
234 | )
235 |
236 | %{to: {zone_to, zone_to_modifier}} = zone_line
237 | NaiveDateTime.compare(rule_to[zone_to_modifier], zone_to) == :gt
238 | end
239 |
240 | defp maybe_pad_left_rule([], _zone_line, _), do: []
241 |
242 | defp maybe_pad_left_rule([first_rule | _] = rules, %{from: :min}, _) do
243 | rule = %{
244 | record_type: :rule,
245 | from: :min,
246 | name: "",
247 | local_offset_from_std_time: 0,
248 | letter: Enum.find(rules, &(&1.local_offset_from_std_time == 0)).letter,
249 | to: first_rule.from
250 | }
251 |
252 | [rule | rules]
253 | end
254 |
255 | defp maybe_pad_left_rule(rules, _zone_line, nil), do: rules
256 |
257 | defp maybe_pad_left_rule([first_rule | _] = rules, zone_line, prev_period) do
258 | {rule_from, rule_from_modifier} = first_rule.from
259 |
260 | if NaiveDateTime.compare(prev_period.to[rule_from_modifier], rule_from) == :lt do
261 | # find first rule with local offset to 0
262 | letter =
263 | case Enum.find(rules, &(&1.local_offset_from_std_time == 0)) do
264 | %{letter: letter} -> letter
265 | _ -> ""
266 | end
267 |
268 | rule = %{
269 | record_type: :rule,
270 | from: zone_line.from,
271 | name: first_rule.name,
272 | local_offset_from_std_time: 0,
273 | letter: letter,
274 | to: first_rule.from
275 | }
276 |
277 | [rule | rules]
278 | else
279 | rules
280 | end
281 | end
282 |
283 | defp do_build_periods_for_zone_line([], _zone_line, _prev_period, periods), do: periods
284 |
285 | defp do_build_periods_for_zone_line([rule | rest_rules], zone_line, prev_period, periods) do
286 | offset_diff =
287 | offset_diff_from_prev_period(zone_line, rule.local_offset_from_std_time, prev_period)
288 |
289 | period_from =
290 | case prev_period do
291 | nil ->
292 | convert_date_tuple(zone_line.from, zone_line.std_offset_from_utc_time, 0)
293 |
294 | %{to: :max} ->
295 | convert_date_tuple(
296 | rule.from,
297 | zone_line.std_offset_from_utc_time,
298 | prev_period.local_offset_from_std_time
299 | )
300 |
301 | _ ->
302 | add_to_and_convert_date_tuple(
303 | {prev_period.to.wall, :wall},
304 | offset_diff,
305 | zone_line.std_offset_from_utc_time,
306 | rule.local_offset_from_std_time
307 | )
308 | end
309 |
310 | period_to =
311 | convert_date_tuple(
312 | rule.to,
313 | zone_line.std_offset_from_utc_time,
314 | rule.local_offset_from_std_time
315 | )
316 |
317 | if period_from != :min && period_to != :max &&
318 | period_from.utc_gregorian_seconds == period_to.utc_gregorian_seconds do
319 | raise "logic error"
320 | end
321 |
322 | period = %{
323 | from: period_from,
324 | to: period_to,
325 | std_offset_from_utc_time: zone_line.std_offset_from_utc_time,
326 | local_offset_from_std_time: rule.local_offset_from_std_time,
327 | zone_abbr: zone_abbr(zone_line, rule.local_offset_from_std_time, rule.letter),
328 | rules_and_template:
329 | if(period_to == :max && prev_period && prev_period.to == :max) do
330 | {zone_line.rules, zone_line.format_time_zone_abbr}
331 | end
332 | }
333 |
334 | periods = concat_dedup_periods([period], periods)
335 |
336 | do_build_periods_for_zone_line(rest_rules, zone_line, period, periods)
337 | end
338 |
339 | defp zone_abbr(zone_line, offset, letter \\ "") do
340 | is_standard_time = offset == 0
341 |
342 | zone_abbr =
343 | cond do
344 | String.contains?(zone_line.format_time_zone_abbr, "/") ->
345 | [zone_abbr_std_time, zone_abbr_dst_time] =
346 | String.split(zone_line.format_time_zone_abbr, "/")
347 |
348 | if(is_standard_time, do: zone_abbr_std_time, else: zone_abbr_dst_time)
349 |
350 | String.contains?(zone_line.format_time_zone_abbr, "%z") ->
351 | total_seconds = zone_line.std_offset_from_utc_time + offset
352 | hours = div(total_seconds, 3600)
353 | minutes = rem(abs(total_seconds), 3600) |> div(60)
354 |
355 | sign = if hours >= 0, do: "+", else: "-"
356 |
357 | if minutes > 0 do
358 | "#{sign}#{abs(hours) |> to_string() |> String.pad_leading(2, "0")}#{abs(minutes) |> to_string() |> String.pad_leading(2, "0")}"
359 | else
360 | "#{sign}#{abs(hours) |> to_string() |> String.pad_leading(2, "0")}"
361 | end
362 |
363 | String.contains?(zone_line.format_time_zone_abbr, "%s") ->
364 | String.replace(zone_line.format_time_zone_abbr, "%s", letter)
365 |
366 | true ->
367 | zone_line.format_time_zone_abbr
368 | end
369 |
370 | unless String.length(zone_abbr) >= 3 and Regex.match?(~r/^[A-Za-z0-9\+\-]+$/, zone_abbr) do
371 | raise "invalid time zone abbreviation #{zone_abbr} for #{zone_line.name}"
372 | end
373 |
374 | zone_abbr
375 | end
376 |
377 | defp add_to_and_convert_date_tuple(
378 | {date, time_modifier},
379 | add_seconds,
380 | std_offset_from_utc_time,
381 | local_offset_from_std_time
382 | ) do
383 | date = NaiveDateTime.add(date, add_seconds, :second)
384 |
385 | convert_date_tuple(
386 | {date, time_modifier},
387 | std_offset_from_utc_time,
388 | local_offset_from_std_time
389 | )
390 | end
391 |
392 | defp convert_date_tuple(:min, _, _), do: :min
393 | defp convert_date_tuple(:max, _, _), do: :max
394 |
395 | defp convert_date_tuple(
396 | {date, time_modifier},
397 | std_offset_from_utc_time,
398 | local_offset_from_std_time
399 | ) do
400 | utc =
401 | convert_date(
402 | date,
403 | std_offset_from_utc_time,
404 | local_offset_from_std_time,
405 | time_modifier,
406 | :utc
407 | )
408 |
409 | %{
410 | utc: utc,
411 | wall:
412 | convert_date(
413 | date,
414 | std_offset_from_utc_time,
415 | local_offset_from_std_time,
416 | time_modifier,
417 | :wall
418 | ),
419 | standard:
420 | convert_date(
421 | date,
422 | std_offset_from_utc_time,
423 | local_offset_from_std_time,
424 | time_modifier,
425 | :standard
426 | )
427 | }
428 | |> Map.put(:utc_gregorian_seconds, naive_datetime_to_gregorian_seconds(utc))
429 | end
430 |
431 | def periods_to_tuples_and_reverse(periods, periods_as_tuples \\ [], prev_period \\ nil)
432 |
433 | def periods_to_tuples_and_reverse([], periods_as_tuples, _), do: periods_as_tuples
434 |
435 | def periods_to_tuples_and_reverse([period | tail], periods_as_tuples, prev_period) do
436 | period = {
437 | if(period.from == :min, do: 0, else: period.from.utc_gregorian_seconds),
438 | {
439 | period.std_offset_from_utc_time,
440 | period.local_offset_from_std_time,
441 | period.zone_abbr
442 | },
443 | prev_period && elem(prev_period, 1),
444 | period[:rules_and_template]
445 | }
446 |
447 | periods_to_tuples_and_reverse(tail, [period | periods_as_tuples], period)
448 | end
449 |
450 | defp convert_date(ndt, _, _, modifier, modifier), do: ndt
451 |
452 | defp convert_date(
453 | ndt,
454 | standard_offset_from_utc_time,
455 | local_offset_from_standard_time,
456 | :wall,
457 | :utc
458 | ) do
459 | NaiveDateTime.add(
460 | ndt,
461 | -1 * (standard_offset_from_utc_time + local_offset_from_standard_time),
462 | :second
463 | )
464 | end
465 |
466 | defp convert_date(
467 | ndt,
468 | _standard_offset_from_utc_time,
469 | local_offset_from_standard_time,
470 | :wall,
471 | :standard
472 | ) do
473 | NaiveDateTime.add(ndt, -1 * local_offset_from_standard_time, :second)
474 | end
475 |
476 | defp convert_date(
477 | ndt,
478 | standard_offset_from_utc_time,
479 | local_offset_from_standard_time,
480 | :utc,
481 | :wall
482 | ) do
483 | NaiveDateTime.add(
484 | ndt,
485 | standard_offset_from_utc_time + local_offset_from_standard_time,
486 | :second
487 | )
488 | end
489 |
490 | defp convert_date(
491 | ndt,
492 | standard_offset_from_utc_time,
493 | _local_offset_from_standard_time,
494 | :utc,
495 | :standard
496 | ) do
497 | NaiveDateTime.add(ndt, standard_offset_from_utc_time, :second)
498 | end
499 |
500 | defp convert_date(
501 | ndt,
502 | standard_offset_from_utc_time,
503 | _local_offset_from_standard_time,
504 | :standard,
505 | :utc
506 | ) do
507 | NaiveDateTime.add(ndt, -1 * standard_offset_from_utc_time, :second)
508 | end
509 |
510 | defp convert_date(
511 | ndt,
512 | _standard_offset_from_utc_time,
513 | local_offset_from_standard_time,
514 | :standard,
515 | :wall
516 | ) do
517 | NaiveDateTime.add(ndt, local_offset_from_standard_time, :second)
518 | end
519 |
520 | defp naive_datetime_to_gregorian_seconds(datetime) do
521 | NaiveDateTime.to_erl(datetime)
522 | |> :calendar.datetime_to_gregorian_seconds()
523 | end
524 | end
525 |
--------------------------------------------------------------------------------
/lib/tz/time_zone_database.ex:
--------------------------------------------------------------------------------
1 | defmodule Tz.TimeZoneDatabase do
2 | @moduledoc false
3 |
4 | @behaviour Calendar.TimeZoneDatabase
5 |
6 | alias Tz.PeriodsProvider
7 |
8 | @compile {:inline, period_to_map: 1}
9 |
10 | @impl true
11 | # called by DateTime.shift_zone/3 and DateTime.add/4
12 | def time_zone_period_from_utc_iso_days(iso_days, time_zone) do
13 | with {:ok, periods} <- PeriodsProvider.periods(time_zone) do
14 | iso_days_to_gregorian_seconds(iso_days)
15 | |> find_period_for_secs(periods, :utc)
16 | end
17 | end
18 |
19 | @impl true
20 | # called by DateTime.from_naive/3
21 | def time_zone_periods_from_wall_datetime(naive_datetime, time_zone) do
22 | with {:ok, periods} <- PeriodsProvider.periods(time_zone) do
23 | naive_datetime_to_gregorian_seconds(naive_datetime)
24 | |> find_period_for_secs(periods, :wall)
25 | end
26 | end
27 |
28 | defp find_period_for_secs(secs, periods, time_modifier) do
29 | case do_find_period_for_secs(secs, periods, time_modifier) do
30 | {:max, utc_offset, rules_and_template} ->
31 | periods = generate_dynamic_periods(secs, utc_offset, rules_and_template)
32 | do_find_period_for_secs(secs, periods, time_modifier)
33 |
34 | result ->
35 | result
36 | end
37 | end
38 |
39 | defp do_find_period_for_secs(secs, periods, :utc) do
40 | case Enum.find(periods, fn {from, _, _, _} -> secs >= from end) do
41 | {_, period, _, nil} ->
42 | {:ok, period_to_map(period)}
43 |
44 | {_, {utc_off, _, _}, _, rules_and_template} ->
45 | {:max, utc_off, rules_and_template}
46 |
47 | nil ->
48 | {_, period, _, _} = List.last(periods)
49 | {:ok, period_to_map(period)}
50 | end
51 | end
52 |
53 | defp do_find_period_for_secs(secs, periods, :wall), do: find_period_for_wall_secs(secs, periods)
54 |
55 | # receives wall gregorian seconds (also referred as the 'given timestamp' in the comments below)
56 | # and the list of transitions
57 | defp find_period_for_wall_secs(_, [{0, period, _, _}]), do: {:ok, period_to_map(period)}
58 |
59 | defp find_period_for_wall_secs(secs, [
60 | {utc_secs, period = {utc_off, std_off, _}, prev_period = {prev_utc_off, prev_std_off, _},
61 | rules_and_template}
62 | | tail
63 | ]) do
64 | # utc_secs + utc_off + std_off = wall gregorian seconds
65 | if secs < utc_secs + utc_off + std_off do
66 | # the given timestamp occurs in a gap if it occurs between
67 | # the utc timestamp + the previous offset and
68 | # the utc timestamp + the offset (= this transition's wall time)
69 | if secs >= utc_secs + prev_utc_off + prev_std_off do
70 | {:gap,
71 | {period_to_map(prev_period),
72 | gregorian_seconds_to_naive_datetime(utc_secs + prev_utc_off + prev_std_off)},
73 | {period_to_map(period),
74 | gregorian_seconds_to_naive_datetime(utc_secs + utc_off + std_off)}}
75 | else
76 | # the given timestamp occurs before this transition and there is no gap with the previous period,
77 | # so continue iterating
78 | find_period_for_wall_secs(secs, tail)
79 | end
80 | else
81 | # the given timestamp occurs during two periods if it occurs between
82 | # the utc timestamp + the offset (= this transition's wall time) and
83 | # the utc timestamp + the previous offset
84 | if secs < utc_secs + prev_utc_off + prev_std_off do
85 | {:ambiguous, period_to_map(prev_period), period_to_map(period)}
86 | else
87 | # the given timestamp occurs after this transition's wall time, and there is no gap nor overlap
88 | case rules_and_template do
89 | nil ->
90 | {:ok, period_to_map(period)}
91 |
92 | _ ->
93 | {:max, utc_off, rules_and_template}
94 | end
95 | end
96 | end
97 | end
98 |
99 | defp period_to_map({utc_off, std_off, abbr}) do
100 | %{
101 | utc_offset: utc_off,
102 | std_offset: std_off,
103 | zone_abbr: abbr
104 | }
105 | end
106 |
107 | @doc false
108 | def generate_dynamic_periods(secs, utc_offset, {rule_name, format_time_zone_abbr}) do
109 | %{year: year} = gregorian_seconds_to_naive_datetime(secs)
110 |
111 | [rule1, rule2] = Tz.OngoingChangingRulesProvider.rules(rule_name)
112 |
113 | rule_records =
114 | Tz.IanaFileParser.denormalized_rule_data([
115 | Tz.IanaFileParser.change_rule_year(rule2, year - 1),
116 | Tz.IanaFileParser.change_rule_year(rule1, year - 1),
117 | Tz.IanaFileParser.change_rule_year(rule2, year),
118 | Tz.IanaFileParser.change_rule_year(rule1, year),
119 | Tz.IanaFileParser.change_rule_year(rule2, year + 1),
120 | Tz.IanaFileParser.change_rule_year(rule1, year + 1)
121 | ])
122 |
123 | zone_line = %{
124 | from: :min,
125 | to: :max,
126 | rules: rule_name,
127 | format_time_zone_abbr: format_time_zone_abbr,
128 | std_offset_from_utc_time: utc_offset
129 | }
130 |
131 | Tz.PeriodsBuilder.build_periods([zone_line], rule_records, :dynamic_far_future)
132 | |> Tz.PeriodsBuilder.periods_to_tuples_and_reverse()
133 | end
134 |
135 | defp iso_days_to_gregorian_seconds({days, {parts_in_day, 86_400_000_000}}) do
136 | div(days * 86_400_000_000 + parts_in_day, 1_000_000)
137 | end
138 |
139 | defp naive_datetime_to_gregorian_seconds(%{calendar: Calendar.ISO, year: year}) when year < 0,
140 | do: 0
141 |
142 | defp naive_datetime_to_gregorian_seconds(%{calendar: Calendar.ISO} = datetime) do
143 | NaiveDateTime.to_erl(datetime)
144 | |> :calendar.datetime_to_gregorian_seconds()
145 | end
146 |
147 | defp naive_datetime_to_gregorian_seconds(datetime) do
148 | datetime
149 | |> NaiveDateTime.convert!(Calendar.ISO)
150 | |> naive_datetime_to_gregorian_seconds()
151 | end
152 |
153 | defp gregorian_seconds_to_naive_datetime(seconds) do
154 | :calendar.gregorian_seconds_to_datetime(seconds)
155 | |> NaiveDateTime.from_erl!()
156 | end
157 | end
158 |
--------------------------------------------------------------------------------
/lib/tz/update_periodically.ex:
--------------------------------------------------------------------------------
1 | defmodule Tz.UpdatePeriodically do
2 | @moduledoc """
3 | A process enabling automatic IANA data updates periodically.
4 | """
5 |
6 | use GenServer
7 | require Logger
8 | alias Tz.HTTP
9 | alias Tz.IanaDataDir
10 | alias Tz.Updater
11 |
12 | defp maybe_recompile() do
13 | Logger.debug("Tz is checking for IANA time zone database updates")
14 |
15 | Updater.maybe_recompile()
16 | end
17 |
18 | @doc false
19 | def start_link(opts) do
20 | if IanaDataDir.forced_iana_version() do
21 | raise "cannot update time zone periods as version #{IanaDataDir.forced_iana_version()} has been forced through the :iana_version config"
22 | end
23 |
24 | HTTP.get_http_client!()
25 |
26 | GenServer.start_link(__MODULE__, opts)
27 | end
28 |
29 | @doc false
30 | def init(opts) do
31 | {:ok, %{opts: opts}, {:continue, :work}}
32 | end
33 |
34 | @doc false
35 | def handle_continue(:work, %{opts: opts}) do
36 | maybe_recompile()
37 | schedule_work(opts[:interval_in_days])
38 | {:noreply, %{opts: opts}}
39 | end
40 |
41 | @doc false
42 | def handle_info(:work, %{opts: opts}) do
43 | maybe_recompile()
44 | schedule_work(opts[:interval_in_days])
45 | {:noreply, %{opts: opts}}
46 | end
47 |
48 | defp schedule_work(interval_in_days) do
49 | interval_in_days = interval_in_days || 1
50 | Process.send_after(self(), :work, interval_in_days * 24 * 60 * 60 * 1000)
51 | end
52 | end
53 |
--------------------------------------------------------------------------------
/lib/tz/updater.ex:
--------------------------------------------------------------------------------
1 | defmodule Tz.Updater do
2 | require Logger
3 |
4 | alias Tz.Compiler
5 | alias Tz.HTTP
6 | alias Tz.HTTP.HTTPResponse
7 | alias Tz.IanaDataDir
8 | alias Tz.PeriodsProvider
9 |
10 | @doc """
11 | Recompiles the period maps only if more recent IANA data is available.
12 | """
13 | def maybe_recompile() do
14 | {_, latest_tz_version} = maybe_update_tz_database_to_latest_version()
15 |
16 | if latest_tz_version != PeriodsProvider.iana_version() do
17 | if IanaDataDir.forced_iana_version() do
18 | raise "cannot update time zone periods as version #{IanaDataDir.forced_iana_version()} has been forced through the :iana_version config"
19 | end
20 |
21 | Logger.info("Tz is recompiling the time zone periods...")
22 |
23 | try do
24 | Code.compiler_options(ignore_module_conflict: true)
25 | Compiler.compile()
26 | Code.compiler_options(ignore_module_conflict: false)
27 | Logger.info("Tz compilation done")
28 | rescue
29 | e ->
30 | Logger.error("""
31 | Failed to recompile IANA time zone data to version #{latest_tz_version}, remaining on current version #{PeriodsProvider.iana_version()}.
32 | Error: #{Exception.format(:error, e, __STACKTRACE__)}
33 | """)
34 | end
35 | end
36 | end
37 |
38 | defp maybe_update_tz_database_to_latest_version() do
39 | latest_version_saved = IanaDataDir.latest_tzdata_version()
40 |
41 | case fetch_latest_iana_tz_version() do
42 | {:ok, latest_version} ->
43 | if latest_version != latest_version_saved &&
44 | latest_version != PeriodsProvider.iana_version() do
45 | Logger.info(
46 | "New IANA time zone data version #{latest_version} available; currently using #{latest_version_saved}."
47 | )
48 |
49 | case update_tz_database(latest_version) do
50 | {:ok, _dir} ->
51 | IanaDataDir.delete_tzdata_dir(latest_version_saved)
52 | {:updated, latest_version}
53 |
54 | :error ->
55 | {:error, latest_version_saved}
56 | end
57 | else
58 | Logger.info(
59 | "Already using the latest IANA time zone data version: #{PeriodsProvider.iana_version()}."
60 | )
61 |
62 | {:already_latest, latest_version}
63 | end
64 |
65 | :error ->
66 | {:error, latest_version_saved}
67 | end
68 | end
69 |
70 | @doc false
71 | def fetch_latest_iana_tz_version() do
72 | Logger.info(
73 | "Tz is fetching the latest IANA time zone data version at https://data.iana.org/time-zones/tzdb/version"
74 | )
75 |
76 | case HTTP.get_http_client!().request("data.iana.org", "/time-zones/tzdb/version") do
77 | %HTTPResponse{body: body, status_code: 200} ->
78 | version = body |> String.trim()
79 | {:ok, version}
80 |
81 | _ ->
82 | Logger.error("Tz failed to read the latest version of the IANA time zone data")
83 | :error
84 | end
85 | end
86 |
87 | @doc false
88 | def update_tz_database(version, dir \\ IanaDataDir.dir())
89 | when is_binary(version) and is_binary(dir) do
90 | case download_tz_database(version) do
91 | {:ok, content} ->
92 | tzdata_dir = IanaDataDir.extract_tzdata_into_dir(version, content, dir)
93 | Logger.info("IANA time zone data extracted into #{tzdata_dir}")
94 | {:ok, tzdata_dir}
95 |
96 | :error ->
97 | :error
98 | end
99 | end
100 |
101 | defp download_tz_database(version) do
102 | Logger.info(
103 | "Tz is downloading the IANA time zone data version #{version} at https://data.iana.org/time-zones/releases/tzdata#{version}.tar.gz"
104 | )
105 |
106 | case HTTP.get_http_client!().request(
107 | "data.iana.org",
108 | "/time-zones/releases/tzdata#{version}.tar.gz"
109 | ) do
110 | %HTTPResponse{body: body, status_code: 200} ->
111 | Logger.info("Tz download done")
112 | {:ok, body}
113 |
114 | _ ->
115 | Logger.error("Tz failed to download the latest IANA time zone data (version #{version})")
116 | :error
117 | end
118 | end
119 | end
120 |
--------------------------------------------------------------------------------
/lib/tz/watch_periodically.ex:
--------------------------------------------------------------------------------
1 | defmodule Tz.WatchPeriodically do
2 | @moduledoc """
3 | A process watching for IANA data updates periodically.
4 | """
5 |
6 | use GenServer
7 | require Logger
8 | alias Tz.HTTP
9 | alias Tz.PeriodsProvider
10 | alias Tz.Updater
11 |
12 | # Logger.warn/1 is deprecated on Elixir 1.15 but since its
13 | # a macro we can replace it without performance penalty by
14 | # selecting the available Logger macro at compile time.
15 | defmacrop logger_warning(message) do
16 | if Code.ensure_loaded?(Logger) && function_exported?(Logger, :"MACRO-warning", 2) do
17 | quote do
18 | Logger.warning(unquote(message))
19 | end
20 | else
21 | quote do
22 | Logger.warn(unquote(message))
23 | end
24 | end
25 | end
26 |
27 | defp watch(on_update_callback) do
28 | Logger.debug("Tz is checking for IANA time zone database updates")
29 |
30 | case Updater.fetch_latest_iana_tz_version() do
31 | {:ok, latest_version} ->
32 | if latest_version != PeriodsProvider.iana_version() do
33 | link = "https://data.iana.org/time-zones/releases/tzdata#{latest_version}.tar.gz"
34 |
35 | logger_warning(
36 | "Tz found a more recent time zone database available for download at #{link}"
37 | )
38 |
39 | on_update_callback && on_update_callback.(latest_version)
40 | else
41 | Logger.info(
42 | "Already using the latest IANA time zone data version: #{PeriodsProvider.iana_version()}."
43 | )
44 | end
45 |
46 | :error ->
47 | Logger.error("Tz failed to read the latest version of the IANA time zone database")
48 | end
49 | end
50 |
51 | @doc false
52 | def start_link(opts) do
53 | HTTP.get_http_client!()
54 |
55 | GenServer.start_link(__MODULE__, opts)
56 | end
57 |
58 | @doc false
59 | def init(opts) do
60 | {:ok, %{opts: opts}, {:continue, :work}}
61 | end
62 |
63 | @doc false
64 | def handle_continue(:work, %{opts: opts}) do
65 | watch(opts[:on_update])
66 | schedule_work(opts[:interval_in_days])
67 | {:noreply, %{opts: opts}}
68 | end
69 |
70 | @doc false
71 | def handle_info(:work, %{opts: opts}) do
72 | watch(opts[:on_update])
73 | schedule_work(opts[:interval_in_days])
74 | {:noreply, %{opts: opts}}
75 | end
76 |
77 | defp schedule_work(interval_in_days) do
78 | interval_in_days = interval_in_days || 1
79 | Process.send_after(self(), :work, interval_in_days * 24 * 60 * 60 * 1000)
80 | end
81 | end
82 |
--------------------------------------------------------------------------------
/mix.exs:
--------------------------------------------------------------------------------
1 | defmodule Tz.MixProject do
2 | use Mix.Project
3 |
4 | @version "0.28.1"
5 |
6 | def project do
7 | [
8 | app: :tz,
9 | elixir: "~> 1.9",
10 | deps: deps(),
11 | elixirc_paths: elixirc_paths(Mix.env()),
12 |
13 | # Hex
14 | version: @version,
15 | package: package(),
16 | description: "Time zone support for Elixir",
17 |
18 | # ExDoc
19 | name: "Tz",
20 | source_url: "https://github.com/mathieuprog/tz",
21 | docs: docs()
22 | ]
23 | end
24 |
25 | def application do
26 | [
27 | extra_applications: [:logger]
28 | ]
29 | end
30 |
31 | defp deps do
32 | [
33 | {:castore, "~> 0.1 or ~> 1.0", optional: true},
34 | {:mint, "~> 1.6", optional: true},
35 | {:ex_doc, "~> 0.34", only: :dev},
36 | {:benchee, "~> 1.3", only: :dev}
37 | ]
38 | end
39 |
40 | defp elixirc_paths(:test), do: ["lib", "test/support"]
41 | defp elixirc_paths(_), do: ["lib"]
42 |
43 | defp package do
44 | [
45 | licenses: ["Apache-2.0"],
46 | maintainers: ["Mathieu Decaffmeyer"],
47 | links: %{
48 | "GitHub" => "https://github.com/mathieuprog/tz",
49 | "Sponsor" => "https://github.com/sponsors/mathieuprog"
50 | }
51 | ]
52 | end
53 |
54 | defp docs do
55 | [
56 | main: "readme",
57 | extras: ["README.md"],
58 | source_ref: "v#{@version}"
59 | ]
60 | end
61 | end
62 |
--------------------------------------------------------------------------------
/mix.lock:
--------------------------------------------------------------------------------
1 | %{
2 | "benchee": {:hex, :benchee, "1.3.1", "c786e6a76321121a44229dde3988fc772bca73ea75170a73fd5f4ddf1af95ccf", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}, {:statistex, "~> 1.0", [hex: :statistex, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "76224c58ea1d0391c8309a8ecbfe27d71062878f59bd41a390266bf4ac1cc56d"},
3 | "castore": {:hex, :castore, "1.0.8", "dedcf20ea746694647f883590b82d9e96014057aff1d44d03ec90f36a5c0dc6e", [:mix], [], "hexpm", "0b2b66d2ee742cb1d9cb8c8be3b43c3a70ee8651f37b75a8b982e036752983f1"},
4 | "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"},
5 | "earmark_parser": {:hex, :earmark_parser, "1.4.41", "ab34711c9dc6212dda44fcd20ecb87ac3f3fce6f0ca2f28d4a00e4154f8cd599", [:mix], [], "hexpm", "a81a04c7e34b6617c2792e291b5a2e57ab316365c2644ddc553bb9ed863ebefa"},
6 | "ex_doc": {:hex, :ex_doc, "0.34.2", "13eedf3844ccdce25cfd837b99bea9ad92c4e511233199440488d217c92571e8", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "5ce5f16b41208a50106afed3de6a2ed34f4acfd65715b82a0b84b49d995f95c1"},
7 | "hpax": {:hex, :hpax, "1.0.0", "28dcf54509fe2152a3d040e4e3df5b265dcb6cb532029ecbacf4ce52caea3fd2", [:mix], [], "hexpm", "7f1314731d711e2ca5fdc7fd361296593fc2542570b3105595bb0bc6d0fad601"},
8 | "makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"},
9 | "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"},
10 | "makeup_erlang": {:hex, :makeup_erlang, "1.0.1", "c7f58c120b2b5aa5fd80d540a89fdf866ed42f1f3994e4fe189abebeab610839", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "8a89a1eeccc2d798d6ea15496a6e4870b75e014d1af514b1b71fa33134f57814"},
11 | "mint": {:hex, :mint, "1.6.2", "af6d97a4051eee4f05b5500671d47c3a67dac7386045d87a904126fd4bbcea2e", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "5ee441dffc1892f1ae59127f74afe8fd82fda6587794278d924e4d90ea3d63f9"},
12 | "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"},
13 | "statistex": {:hex, :statistex, "1.0.0", "f3dc93f3c0c6c92e5f291704cf62b99b553253d7969e9a5fa713e5481cd858a5", [:mix], [], "hexpm", "ff9d8bee7035028ab4742ff52fc80a2aa35cece833cf5319009b52f1b5a86c27"},
14 | }
15 |
--------------------------------------------------------------------------------
/priv/tzdata2024b/antarctica:
--------------------------------------------------------------------------------
1 | # tzdb data for Antarctica and environs
2 |
3 | # This file is in the public domain, so clarified as of
4 | # 2009-05-17 by Arthur David Olson.
5 |
6 | # From Paul Eggert (1999-11-15):
7 | # To keep things manageable, we list only locations occupied year-round; see
8 | # COMNAP - Stations and Bases
9 | # http://www.comnap.aq/comnap/comnap.nsf/P/Stations/
10 | # and
11 | # Summary of the Peri-Antarctic Islands (1998-07-23)
12 | # http://www.spri.cam.ac.uk/bob/periant.htm
13 | # for information.
14 | # Unless otherwise specified, we have no time zone information.
15 |
16 | # FORMAT is '-00' and STDOFF is 0 for locations while uninhabited.
17 |
18 | # Argentina - year-round bases
19 | # Belgrano II, Confin Coast, -770227-0343737, since 1972-02-05
20 | # Carlini, Potter Cove, King George Island, -6414-0602320, since 1982-01
21 | # Esperanza, Hope Bay, -6323-05659, since 1952-12-17
22 | # Marambio, -6414-05637, since 1969-10-29
23 | # Orcadas, Laurie I, -6016-04444, since 1904-02-22
24 | # San Martín, Barry I, -6808-06706, since 1951-03-21
25 | # (except 1960-03 / 1976-03-21)
26 |
27 | # Australia - territories
28 | # Heard Island, McDonald Islands (uninhabited)
29 | # previously sealers and scientific personnel wintered
30 | # Margaret Turner reports
31 | # https://web.archive.org/web/20021204222245/http://www.dstc.qut.edu.au/DST/marg/daylight.html
32 | # (1999-09-30) that they're UT +05, with no DST;
33 | # presumably this is when they have visitors.
34 | #
35 | # year-round bases
36 | # Casey, Bailey Peninsula, -6617+11032, since 1969
37 | # Davis, Vestfold Hills, -6835+07759, since 1957-01-13
38 | # (except 1964-11 - 1969-02)
39 | # Mawson, Holme Bay, -6736+06253, since 1954-02-13
40 |
41 | # From Steffen Thorsen (2009-03-11):
42 | # Three Australian stations in Antarctica have changed their time zone:
43 | # Casey moved from UTC+8 to UTC+11
44 | # Davis moved from UTC+7 to UTC+5
45 | # Mawson moved from UTC+6 to UTC+5
46 | # The changes occurred on 2009-10-18 at 02:00 (local times).
47 | #
48 | # Government source: (Australian Antarctic Division)
49 | # http://www.aad.gov.au/default.asp?casid=37079
50 | #
51 | # We have more background information here:
52 | # https://www.timeanddate.com/news/time/antarctica-new-times.html
53 |
54 | # From Steffen Thorsen (2010-03-10):
55 | # We got these changes from the Australian Antarctic Division: ...
56 | #
57 | # - Casey station reverted to its normal time of UTC+8 on 5 March 2010.
58 | # The change to UTC+11 is being considered as a regular summer thing but
59 | # has not been decided yet.
60 | #
61 | # - Davis station will revert to its normal time of UTC+7 at 10 March 2010
62 | # 20:00 UTC.
63 | #
64 | # - Mawson station stays on UTC+5.
65 | #
66 | # Background:
67 | # https://www.timeanddate.com/news/time/antartica-time-changes-2010.html
68 |
69 | # From Steffen Thorsen (2016-10-28):
70 | # Australian Antarctica Division informed us that Casey changed time
71 | # zone to UTC+11 in "the morning of 22nd October 2016".
72 |
73 | # From Steffen Thorsen (2020-10-02, as corrected):
74 | # Based on information we have received from the Australian Antarctic
75 | # Division, Casey station and Macquarie Island station will move to Tasmanian
76 | # daylight savings time on Sunday 4 October. This will take effect from 0001
77 | # hrs on Sunday 4 October 2020 and will mean Casey and Macquarie Island will
78 | # be on the same time zone as Hobart. Some past dates too for this 3 hour
79 | # time change back and forth between UTC+8 and UTC+11 for Casey:
80 | # - 2018 Oct 7 4:00 - 2019 Mar 17 3:00 - 2019 Oct 4 3:00 - 2020 Mar 8 3:00
81 | # and now - 2020 Oct 4 0:01
82 |
83 | # From Paul Eggert (2023-12-20):
84 | # Transitions from 2021 on are taken from:
85 | # https://www.timeanddate.com/time/zone/antarctica/casey
86 | # retrieved at various dates.
87 |
88 | # Zone NAME STDOFF RULES FORMAT [UNTIL]
89 | Zone Antarctica/Casey 0 - -00 1969
90 | 8:00 - %z 2009 Oct 18 2:00
91 | 11:00 - %z 2010 Mar 5 2:00
92 | 8:00 - %z 2011 Oct 28 2:00
93 | 11:00 - %z 2012 Feb 21 17:00u
94 | 8:00 - %z 2016 Oct 22
95 | 11:00 - %z 2018 Mar 11 4:00
96 | 8:00 - %z 2018 Oct 7 4:00
97 | 11:00 - %z 2019 Mar 17 3:00
98 | 8:00 - %z 2019 Oct 4 3:00
99 | 11:00 - %z 2020 Mar 8 3:00
100 | 8:00 - %z 2020 Oct 4 0:01
101 | 11:00 - %z 2021 Mar 14 0:00
102 | 8:00 - %z 2021 Oct 3 0:01
103 | 11:00 - %z 2022 Mar 13 0:00
104 | 8:00 - %z 2022 Oct 2 0:01
105 | 11:00 - %z 2023 Mar 9 3:00
106 | 8:00 - %z
107 | Zone Antarctica/Davis 0 - -00 1957 Jan 13
108 | 7:00 - %z 1964 Nov
109 | 0 - -00 1969 Feb
110 | 7:00 - %z 2009 Oct 18 2:00
111 | 5:00 - %z 2010 Mar 10 20:00u
112 | 7:00 - %z 2011 Oct 28 2:00
113 | 5:00 - %z 2012 Feb 21 20:00u
114 | 7:00 - %z
115 | Zone Antarctica/Mawson 0 - -00 1954 Feb 13
116 | 6:00 - %z 2009 Oct 18 2:00
117 | 5:00 - %z
118 | # References:
119 | # Casey Weather (1998-02-26)
120 | # http://www.antdiv.gov.au/aad/exop/sfo/casey/casey_aws.html
121 | # Davis Station, Antarctica (1998-02-26)
122 | # http://www.antdiv.gov.au/aad/exop/sfo/davis/video.html
123 | # Mawson Station, Antarctica (1998-02-25)
124 | # http://www.antdiv.gov.au/aad/exop/sfo/mawson/video.html
125 |
126 | # Belgium - year-round base
127 | # Princess Elisabeth, Queen Maud Land, -713412+0231200, since 2007
128 |
129 | # Brazil - year-round base
130 | # Ferraz, King George Island, -6205+05824, since 1983/4
131 |
132 | # Bulgaria - year-round base
133 | # St. Kliment Ohridski, Livingston Island, -623829-0602153, since 1988
134 |
135 | # Chile - year-round bases and towns
136 | # Escudero, South Shetland Is, -621157-0585735, since 1994
137 | # Frei Montalva, King George Island, -6214-05848, since 1969-03-07
138 | # O'Higgins, Antarctic Peninsula, -6319-05704, since 1948-02
139 | # Prat, -6230-05941
140 | # Villa Las Estrellas (a town), around the Frei base, since 1984-04-09
141 | # These locations employ Region of Magallanes time; use
142 | # TZ='America/Punta_Arenas'.
143 |
144 | # China - year-round bases
145 | # Great Wall, King George Island, -6213-05858, since 1985-02-20
146 | # Zhongshan, Larsemann Hills, Prydz Bay, -6922+07623, since 1989-02-26
147 |
148 | # France - year-round bases (also see "France & Italy")
149 | #
150 | # From Antoine Leca (1997-01-20):
151 | # Time data entries are from Nicole Pailleau at the IFRTP
152 | # (French Institute for Polar Research and Technology).
153 | # She confirms that French Southern Territories and Terre Adélie bases
154 | # don't observe daylight saving time, even if Terre Adélie supplies came
155 | # from Tasmania.
156 | #
157 | # French Southern Territories with year-round inhabitants
158 | #
159 | # Alfred Faure, Possession Island, Crozet Islands, -462551+0515152, since 1964;
160 | # sealing & whaling stations operated variously 1802/1911+;
161 | # see Asia/Dubai.
162 | #
163 | # Martin-de-Viviès, Amsterdam Island, -374105+0773155, since 1950
164 | # Port-aux-Français, Kerguelen Islands, -492110+0701303, since 1951;
165 | # whaling & sealing station operated 1908/1914, 1920/1929, and 1951/1956
166 | #
167 | # St Paul Island - near Amsterdam, uninhabited
168 | # fishing stations operated variously 1819/1931
169 | #
170 | # Kerguelen - see Indian/Maldives.
171 | #
172 | # year-round base in the main continent
173 | # Dumont d'Urville - see Pacific/Port_Moresby.
174 |
175 | # France & Italy - year-round base
176 | # Concordia, -750600+1232000, since 2005
177 |
178 | # Germany - year-round base
179 | # Neumayer III, -704080-0081602, since 2009
180 |
181 | # India - year-round bases
182 | # Bharati, -692428+0761114, since 2012
183 | # Maitri, -704558+0114356, since 1989
184 |
185 | # Italy - year-round base (also see "France & Italy")
186 | # Zuchelli, Terra Nova Bay, -744140+1640647, since 1986
187 |
188 | # Japan - year-round bases
189 | # See Asia/Riyadh.
190 |
191 | # S Korea - year-round base
192 | # Jang Bogo, Terra Nova Bay, -743700+1641205 since 2014
193 | # King Sejong, King George Island, -6213-05847, since 1988
194 |
195 | # New Zealand - claims
196 | # Balleny Islands (never inhabited)
197 | # Scott Island (never inhabited)
198 | #
199 | # year-round base
200 | # Scott Base, Ross Island, since 1957-01.
201 | # See Pacific/Auckland.
202 |
203 | # Norway - territories
204 | # Bouvet (never inhabited)
205 | #
206 | # claims
207 | # Peter I Island (never inhabited)
208 | #
209 | # year-round base
210 | # Troll, Queen Maud Land, -720041+0023206, since 2005-02-12
211 | #
212 | # From Paul-Inge Flakstad (2014-03-10):
213 | # I recently had a long dialog about this with the developer of timegenie.com.
214 | # In the absence of specific dates, he decided to choose some likely ones:
215 | # GMT +1 - From March 1 to the last Sunday in March
216 | # GMT +2 - From the last Sunday in March until the last Sunday in October
217 | # GMT +1 - From the last Sunday in October until November 7
218 | # GMT +0 - From November 7 until March 1
219 | # The dates for switching to and from UTC+0 will probably not be absolutely
220 | # correct, but they should be quite close to the actual dates.
221 | #
222 | # From Paul Eggert (2014-03-21):
223 | # The CET-switching Troll rules require zic from tz 2014b or later, so as
224 | # suggested by Bengt-Inge Larsson comment them out for now, and approximate
225 | # with only UTC and CEST. Uncomment them when 2014b is more prevalent.
226 | #
227 | # Rule NAME FROM TO - IN ON AT SAVE LETTER/S
228 | #Rule Troll 2005 max - Mar 1 1:00u 1:00 +01
229 | Rule Troll 2005 max - Mar lastSun 1:00u 2:00 +02
230 | #Rule Troll 2005 max - Oct lastSun 1:00u 1:00 +01
231 | #Rule Troll 2004 max - Nov 7 1:00u 0:00 +00
232 | # Remove the following line when uncommenting the above '#Rule' lines.
233 | Rule Troll 2004 max - Oct lastSun 1:00u 0:00 +00
234 | # Zone NAME STDOFF RULES FORMAT [UNTIL]
235 | Zone Antarctica/Troll 0 - -00 2005 Feb 12
236 | 0:00 Troll %s
237 |
238 | # Poland - year-round base
239 | # Arctowski, King George Island, -620945-0582745, since 1977
240 |
241 | # Romania - year-bound base
242 | # Law-Racoviță, Larsemann Hills, -692319+0762251, since 1986
243 |
244 | # Russia - year-round bases
245 | # Bellingshausen, King George Island, -621159-0585337, since 1968-02-22
246 | # Mirny, Davis coast, -6633+09301, since 1956-02
247 | # Molodezhnaya, Alasheyev Bay, -6740+04551,
248 | # year-round from 1962-02 to 1999-07-01
249 | # Novolazarevskaya, Queen Maud Land, -7046+01150,
250 | # year-round from 1960/61 to 1992
251 |
252 | # Vostok, since 1957-12-16, temporarily closed 1994-02/1994-11
253 | # From Craig Mundell (1994-12-15):
254 | # http://quest.arc.nasa.gov/antarctica/QA/computers/Directions,Time,ZIP
255 | # Vostok, which is one of the Russian stations, is set on the same
256 | # time as Moscow, Russia.
257 | #
258 | # From Lee Hotz (2001-03-08):
259 | # I queried the folks at Columbia who spent the summer at Vostok and this is
260 | # what they had to say about time there:
261 | # "in the US Camp (East Camp) we have been on New Zealand (McMurdo)
262 | # time, which is 12 hours ahead of GMT. The Russian Station Vostok was
263 | # 6 hours behind that (although only 2 miles away, i.e. 6 hours ahead
264 | # of GMT). This is a time zone I think two hours east of Moscow. The
265 | # natural time zone is in between the two: 8 hours ahead of GMT."
266 | #
267 | # From Paul Eggert (2001-05-04):
268 | # This seems to be hopelessly confusing, so I asked Lee Hotz about it
269 | # in person. He said that some Antarctic locations set their local
270 | # time so that noon is the warmest part of the day, and that this
271 | # changes during the year and does not necessarily correspond to mean
272 | # solar noon. So the Vostok time might have been whatever the clocks
273 | # happened to be during their visit. So we still don't really know what time
274 | # it is at Vostok.
275 | #
276 | # From Zakhary V. Akulov (2023-12-17 22:00:48 +0700):
277 | # ... from December, 18, 2023 00:00 by my decision the local time of
278 | # the Antarctic research base Vostok will correspond to UTC+5.
279 | # (2023-12-19): We constantly interact with Progress base, with company who
280 | # builds new wintering station, with sledge convoys, with aviation - they all
281 | # use UTC+5. Besides, difference between Moscow time is just 2 hours now, not 4.
282 | # (2023-12-19, in response to the question "Has local time at Vostok
283 | # been UTC+6 ever since 1957, or has it changed before?"): No. At least
284 | # since my antarctic career start, 10 years ago, Vostok base has UTC+7.
285 | # (In response to a 2023-12-18 question "from 02:00 to 00:00 today"): This.
286 | #
287 | # From Paul Eggert (2023-12-18):
288 | # For lack of better info, guess Vostok was at +07 from founding through today,
289 | # except when closed.
290 |
291 | # Zone NAME STDOFF RULES FORMAT [UNTIL]
292 | Zone Antarctica/Vostok 0 - -00 1957 Dec 16
293 | 7:00 - %z 1994 Feb
294 | 0 - -00 1994 Nov
295 | 7:00 - %z 2023 Dec 18 2:00
296 | 5:00 - %z
297 |
298 | # S Africa - year-round bases
299 | # Marion Island, -4653+03752
300 | # SANAE IV, Vesleskarvet, Queen Maud Land, -714022-0025026, since 1997
301 |
302 | # Ukraine - year-round base
303 | # Vernadsky (formerly Faraday), Galindez Island, -651445-0641526, since 1954
304 |
305 | # United Kingdom
306 | #
307 | # British Antarctic Territories (BAT) claims
308 | # South Orkney Islands
309 | # scientific station from 1903
310 | # whaling station at Signy I 1920/1926
311 | # South Shetland Islands
312 | #
313 | # year-round bases
314 | # Bird Island, South Georgia, -5400-03803, since 1983
315 | # Deception Island, -6259-06034, whaling station 1912/1931,
316 | # scientific station 1943/1967,
317 | # previously sealers and a scientific expedition wintered by accident,
318 | # and a garrison was deployed briefly
319 | # Halley, Coates Land, -7535-02604, since 1956-01-06
320 | # Halley is on a moving ice shelf and is periodically relocated
321 | # so that it is never more than 10km from its nominal location.
322 | # Rothera, Adelaide Island, -6734-6808, since 1976-12-01
323 | #
324 | # From Paul Eggert (2002-10-22)
325 | # says Rothera is -03 all year.
326 | #
327 | # Zone NAME STDOFF RULES FORMAT [UNTIL]
328 | Zone Antarctica/Rothera 0 - -00 1976 Dec 1
329 | -3:00 - %z
330 |
331 | # Uruguay - year round base
332 | # Artigas, King George Island, -621104-0585107
333 |
334 | # USA - year-round bases
335 | #
336 | # Palmer, Anvers Island, since 1965 (moved 2 miles in 1968)
337 | # See 'southamerica' for Antarctica/Palmer, since it uses South American DST.
338 | #
339 | # McMurdo Station, Ross Island, since 1955-12
340 | # Amundsen-Scott South Pole Station, continuously occupied since 1956-11-20
341 | #
342 | # From Chris Carrier (1996-06-27):
343 | # Siple, the first commander of the South Pole station,
344 | # stated that he would have liked to have kept GMT at the station,
345 | # but that he found it more convenient to keep GMT+12
346 | # as supplies for the station were coming from McMurdo Sound,
347 | # which was on GMT+12 because New Zealand was on GMT+12 all year
348 | # at that time (1957). (Source: Siple's book 90° South.)
349 | #
350 | # From Susan Smith
351 | # http://www.cybertours.com/whs/pole10.html
352 | # (1995-11-13 16:24:56 +1300, no longer available):
353 | # We use the same time as McMurdo does.
354 | # And they use the same time as Christchurch, NZ does....
355 | # One last quirk about South Pole time.
356 | # All the electric clocks are usually wrong.
357 | # Something about the generators running at 60.1hertz or something
358 | # makes all of the clocks run fast. So every couple of days,
359 | # we have to go around and set them back 5 minutes or so.
360 | # Maybe if we let them run fast all of the time, we'd get to leave here sooner!!
361 | #
362 | # See Pacific/Auckland.
363 |
--------------------------------------------------------------------------------
/priv/tzdata2024b/backward:
--------------------------------------------------------------------------------
1 | # Links and zones for backward compatibility
2 |
3 | # This file is in the public domain, so clarified as of
4 | # 2009-05-17 by Arthur David Olson.
5 |
6 | # This file provides links from old or merged timezone names to current ones.
7 | # It also provides a few zone entries for old naming conventions.
8 | # Many names changed in 1993 and in 1995, and many merged names moved here
9 | # in the period from 2013 through 2022. Several of these names are
10 | # also present in the file 'backzone', which has data important only
11 | # for pre-1970 timestamps and so is out of scope for tzdb proper.
12 |
13 | # Although this file is optional and tzdb will work if you omit it by
14 | # building with 'make BACKWARD=', in practice downstream users
15 | # typically use this file for backward compatibility.
16 |
17 | # This file is divided into sections, one for each major reason for a
18 | # backward compatibility link. Each section is sorted by link name.
19 |
20 | # A "#= TARGET1" comment labels each link inserted only because some
21 | # .zi parsers (including tzcode through 2022e) mishandle links to links.
22 | # The comment says what the target would be if these parsers were fixed
23 | # so that data could contain links to links. For example, the line
24 | # "Link Australia/Sydney Australia/ACT #= Australia/Canberra" would be
25 | # "Link Australia/Canberra Australia/ACT" were it not that data lines
26 | # refrain from linking to links like Australia/Canberra, which means
27 | # the Australia/ACT line links instead to Australia/Sydney,
28 | # Australia/Canberra's target.
29 |
30 |
31 | # Pre-1993 naming conventions
32 |
33 | # Link TARGET LINK-NAME #= TARGET1
34 | Link Australia/Sydney Australia/ACT #= Australia/Canberra
35 | Link Australia/Lord_Howe Australia/LHI
36 | Link Australia/Sydney Australia/NSW
37 | Link Australia/Darwin Australia/North
38 | Link Australia/Brisbane Australia/Queensland
39 | Link Australia/Adelaide Australia/South
40 | Link Australia/Hobart Australia/Tasmania
41 | Link Australia/Melbourne Australia/Victoria
42 | Link Australia/Perth Australia/West
43 | Link Australia/Broken_Hill Australia/Yancowinna
44 | Link America/Rio_Branco Brazil/Acre #= America/Porto_Acre
45 | Link America/Noronha Brazil/DeNoronha
46 | Link America/Sao_Paulo Brazil/East
47 | Link America/Manaus Brazil/West
48 | Link Europe/Brussels CET
49 | Link America/Chicago CST6CDT
50 | Link America/Halifax Canada/Atlantic
51 | Link America/Winnipeg Canada/Central
52 | # This line is commented out, as the name exceeded the 14-character limit
53 | # and was an unused misnomer.
54 | #Link America/Regina Canada/East-Saskatchewan
55 | Link America/Toronto Canada/Eastern
56 | Link America/Edmonton Canada/Mountain
57 | Link America/St_Johns Canada/Newfoundland
58 | Link America/Vancouver Canada/Pacific
59 | Link America/Regina Canada/Saskatchewan
60 | Link America/Whitehorse Canada/Yukon
61 | Link America/Santiago Chile/Continental
62 | Link Pacific/Easter Chile/EasterIsland
63 | Link America/Havana Cuba
64 | Link Europe/Athens EET
65 | Link America/Panama EST
66 | Link America/New_York EST5EDT
67 | Link Africa/Cairo Egypt
68 | Link Europe/Dublin Eire
69 | # Vanguard section, for most .zi parsers.
70 | #Link GMT Etc/GMT
71 | #Link GMT Etc/GMT+0
72 | #Link GMT Etc/GMT-0
73 | #Link GMT Etc/GMT0
74 | #Link GMT Etc/Greenwich
75 | # Rearguard section, for TZUpdater 2.3.2 and earlier.
76 | Link Etc/GMT Etc/GMT+0
77 | Link Etc/GMT Etc/GMT-0
78 | Link Etc/GMT Etc/GMT0
79 | Link Etc/GMT Etc/Greenwich
80 | # End of rearguard section.
81 | Link Etc/UTC Etc/UCT
82 | Link Etc/UTC Etc/Universal
83 | Link Etc/UTC Etc/Zulu
84 | Link Europe/London GB
85 | Link Europe/London GB-Eire
86 | # Vanguard section, for most .zi parsers.
87 | #Link GMT GMT+0
88 | #Link GMT GMT-0
89 | #Link GMT GMT0
90 | #Link GMT Greenwich
91 | # Rearguard section, for TZUpdater 2.3.2 and earlier.
92 | Link Etc/GMT GMT+0
93 | Link Etc/GMT GMT-0
94 | Link Etc/GMT GMT0
95 | Link Etc/GMT Greenwich
96 | # End of rearguard section.
97 | Link Asia/Hong_Kong Hongkong
98 | Link Africa/Abidjan Iceland #= Atlantic/Reykjavik
99 | Link Asia/Tehran Iran
100 | Link Asia/Jerusalem Israel
101 | Link America/Jamaica Jamaica
102 | Link Asia/Tokyo Japan
103 | Link Pacific/Kwajalein Kwajalein
104 | Link Africa/Tripoli Libya
105 | Link Europe/Brussels MET
106 | Link America/Phoenix MST
107 | Link America/Denver MST7MDT
108 | Link America/Tijuana Mexico/BajaNorte
109 | Link America/Mazatlan Mexico/BajaSur
110 | Link America/Mexico_City Mexico/General
111 | Link Pacific/Auckland NZ
112 | Link Pacific/Chatham NZ-CHAT
113 | Link America/Denver Navajo #= America/Shiprock
114 | Link Asia/Shanghai PRC
115 | Link Europe/Warsaw Poland
116 | Link Europe/Lisbon Portugal
117 | Link Asia/Taipei ROC
118 | Link Asia/Seoul ROK
119 | Link Asia/Singapore Singapore
120 | Link Europe/Istanbul Turkey
121 | Link Etc/UTC UCT
122 | Link America/Anchorage US/Alaska
123 | Link America/Adak US/Aleutian
124 | Link America/Phoenix US/Arizona
125 | Link America/Chicago US/Central
126 | Link America/Indiana/Indianapolis US/East-Indiana
127 | Link America/New_York US/Eastern
128 | Link Pacific/Honolulu US/Hawaii
129 | Link America/Indiana/Knox US/Indiana-Starke
130 | Link America/Detroit US/Michigan
131 | Link America/Denver US/Mountain
132 | Link America/Los_Angeles US/Pacific
133 | Link Pacific/Pago_Pago US/Samoa
134 | Link Etc/UTC UTC
135 | Link Etc/UTC Universal
136 | Link Europe/Moscow W-SU
137 | Link Etc/UTC Zulu
138 |
139 |
140 | # Two-part names that were renamed mostly to three-part names in 1995
141 |
142 | # Link TARGET LINK-NAME #= TARGET1
143 | Link America/Argentina/Buenos_Aires America/Buenos_Aires
144 | Link America/Argentina/Catamarca America/Catamarca
145 | Link America/Argentina/Cordoba America/Cordoba
146 | Link America/Indiana/Indianapolis America/Indianapolis
147 | Link America/Argentina/Jujuy America/Jujuy
148 | Link America/Indiana/Knox America/Knox_IN
149 | Link America/Kentucky/Louisville America/Louisville
150 | Link America/Argentina/Mendoza America/Mendoza
151 | Link America/Puerto_Rico America/Virgin #= America/St_Thomas
152 | Link Pacific/Pago_Pago Pacific/Samoa
153 |
154 |
155 | # Pre-2013 practice, which typically had a Zone per zone.tab line
156 |
157 | # Link TARGET LINK-NAME
158 | Link Africa/Abidjan Africa/Accra
159 | Link Africa/Nairobi Africa/Addis_Ababa
160 | Link Africa/Nairobi Africa/Asmara
161 | Link Africa/Abidjan Africa/Bamako
162 | Link Africa/Lagos Africa/Bangui
163 | Link Africa/Abidjan Africa/Banjul
164 | Link Africa/Maputo Africa/Blantyre
165 | Link Africa/Lagos Africa/Brazzaville
166 | Link Africa/Maputo Africa/Bujumbura
167 | Link Africa/Abidjan Africa/Conakry
168 | Link Africa/Abidjan Africa/Dakar
169 | Link Africa/Nairobi Africa/Dar_es_Salaam
170 | Link Africa/Nairobi Africa/Djibouti
171 | Link Africa/Lagos Africa/Douala
172 | Link Africa/Abidjan Africa/Freetown
173 | Link Africa/Maputo Africa/Gaborone
174 | Link Africa/Maputo Africa/Harare
175 | Link Africa/Nairobi Africa/Kampala
176 | Link Africa/Maputo Africa/Kigali
177 | Link Africa/Lagos Africa/Kinshasa
178 | Link Africa/Lagos Africa/Libreville
179 | Link Africa/Abidjan Africa/Lome
180 | Link Africa/Lagos Africa/Luanda
181 | Link Africa/Maputo Africa/Lubumbashi
182 | Link Africa/Maputo Africa/Lusaka
183 | Link Africa/Lagos Africa/Malabo
184 | Link Africa/Johannesburg Africa/Maseru
185 | Link Africa/Johannesburg Africa/Mbabane
186 | Link Africa/Nairobi Africa/Mogadishu
187 | Link Africa/Lagos Africa/Niamey
188 | Link Africa/Abidjan Africa/Nouakchott
189 | Link Africa/Abidjan Africa/Ouagadougou
190 | Link Africa/Lagos Africa/Porto-Novo
191 | Link America/Puerto_Rico America/Anguilla
192 | Link America/Puerto_Rico America/Antigua
193 | Link America/Puerto_Rico America/Aruba
194 | Link America/Panama America/Atikokan
195 | Link America/Puerto_Rico America/Blanc-Sablon
196 | Link America/Panama America/Cayman
197 | Link America/Phoenix America/Creston
198 | Link America/Puerto_Rico America/Curacao
199 | Link America/Puerto_Rico America/Dominica
200 | Link America/Puerto_Rico America/Grenada
201 | Link America/Puerto_Rico America/Guadeloupe
202 | Link America/Puerto_Rico America/Kralendijk
203 | Link America/Puerto_Rico America/Lower_Princes
204 | Link America/Puerto_Rico America/Marigot
205 | Link America/Puerto_Rico America/Montserrat
206 | Link America/Toronto America/Nassau
207 | Link America/Puerto_Rico America/Port_of_Spain
208 | Link America/Puerto_Rico America/St_Barthelemy
209 | Link America/Puerto_Rico America/St_Kitts
210 | Link America/Puerto_Rico America/St_Lucia
211 | Link America/Puerto_Rico America/St_Thomas
212 | Link America/Puerto_Rico America/St_Vincent
213 | Link America/Puerto_Rico America/Tortola
214 | Link Pacific/Port_Moresby Antarctica/DumontDUrville
215 | Link Pacific/Auckland Antarctica/McMurdo
216 | Link Asia/Riyadh Antarctica/Syowa
217 | Link Europe/Berlin Arctic/Longyearbyen
218 | Link Asia/Riyadh Asia/Aden
219 | Link Asia/Qatar Asia/Bahrain
220 | Link Asia/Kuching Asia/Brunei
221 | Link Asia/Singapore Asia/Kuala_Lumpur
222 | Link Asia/Riyadh Asia/Kuwait
223 | Link Asia/Dubai Asia/Muscat
224 | Link Asia/Bangkok Asia/Phnom_Penh
225 | Link Asia/Bangkok Asia/Vientiane
226 | Link Africa/Abidjan Atlantic/Reykjavik
227 | Link Africa/Abidjan Atlantic/St_Helena
228 | Link Europe/Brussels Europe/Amsterdam
229 | Link Europe/Prague Europe/Bratislava
230 | Link Europe/Zurich Europe/Busingen
231 | Link Europe/Berlin Europe/Copenhagen
232 | Link Europe/London Europe/Guernsey
233 | Link Europe/London Europe/Isle_of_Man
234 | Link Europe/London Europe/Jersey
235 | Link Europe/Belgrade Europe/Ljubljana
236 | Link Europe/Brussels Europe/Luxembourg
237 | Link Europe/Helsinki Europe/Mariehamn
238 | Link Europe/Paris Europe/Monaco
239 | Link Europe/Berlin Europe/Oslo
240 | Link Europe/Belgrade Europe/Podgorica
241 | Link Europe/Rome Europe/San_Marino
242 | Link Europe/Belgrade Europe/Sarajevo
243 | Link Europe/Belgrade Europe/Skopje
244 | Link Europe/Berlin Europe/Stockholm
245 | Link Europe/Zurich Europe/Vaduz
246 | Link Europe/Rome Europe/Vatican
247 | Link Europe/Belgrade Europe/Zagreb
248 | Link Africa/Nairobi Indian/Antananarivo
249 | Link Asia/Bangkok Indian/Christmas
250 | Link Asia/Yangon Indian/Cocos
251 | Link Africa/Nairobi Indian/Comoro
252 | Link Indian/Maldives Indian/Kerguelen
253 | Link Asia/Dubai Indian/Mahe
254 | Link Africa/Nairobi Indian/Mayotte
255 | Link Asia/Dubai Indian/Reunion
256 | Link Pacific/Port_Moresby Pacific/Chuuk
257 | Link Pacific/Tarawa Pacific/Funafuti
258 | Link Pacific/Tarawa Pacific/Majuro
259 | Link Pacific/Pago_Pago Pacific/Midway
260 | Link Pacific/Guadalcanal Pacific/Pohnpei
261 | Link Pacific/Guam Pacific/Saipan
262 | Link Pacific/Tarawa Pacific/Wake
263 | Link Pacific/Tarawa Pacific/Wallis
264 |
265 |
266 | # Non-zone.tab locations with timestamps since 1970 that duplicate
267 | # those of an existing location
268 |
269 | # Link TARGET LINK-NAME
270 | Link Africa/Abidjan Africa/Timbuktu
271 | Link America/Argentina/Catamarca America/Argentina/ComodRivadavia
272 | Link America/Adak America/Atka
273 | Link America/Panama America/Coral_Harbour
274 | Link America/Tijuana America/Ensenada
275 | Link America/Indiana/Indianapolis America/Fort_Wayne
276 | Link America/Toronto America/Montreal
277 | Link America/Toronto America/Nipigon
278 | Link America/Iqaluit America/Pangnirtung
279 | Link America/Rio_Branco America/Porto_Acre
280 | Link America/Winnipeg America/Rainy_River
281 | Link America/Argentina/Cordoba America/Rosario
282 | Link America/Tijuana America/Santa_Isabel
283 | Link America/Denver America/Shiprock
284 | Link America/Toronto America/Thunder_Bay
285 | Link America/Edmonton America/Yellowknife
286 | Link Pacific/Auckland Antarctica/South_Pole
287 | Link Asia/Ulaanbaatar Asia/Choibalsan
288 | Link Asia/Shanghai Asia/Chongqing
289 | Link Asia/Shanghai Asia/Harbin
290 | Link Asia/Urumqi Asia/Kashgar
291 | Link Asia/Jerusalem Asia/Tel_Aviv
292 | Link Europe/Berlin Atlantic/Jan_Mayen
293 | Link Australia/Sydney Australia/Canberra
294 | Link Australia/Hobart Australia/Currie
295 | Link Europe/London Europe/Belfast
296 | Link Europe/Chisinau Europe/Tiraspol
297 | Link Europe/Kyiv Europe/Uzhgorod
298 | Link Europe/Kyiv Europe/Zaporozhye
299 | Link Pacific/Kanton Pacific/Enderbury
300 | Link Pacific/Honolulu Pacific/Johnston
301 | Link Pacific/Port_Moresby Pacific/Yap
302 | Link Europe/Lisbon WET
303 |
304 |
305 | # Alternate names for the same location
306 |
307 | # Link TARGET LINK-NAME #= TARGET1
308 | Link Africa/Nairobi Africa/Asmera #= Africa/Asmara
309 | Link America/Nuuk America/Godthab
310 | Link Asia/Ashgabat Asia/Ashkhabad
311 | Link Asia/Kolkata Asia/Calcutta
312 | Link Asia/Shanghai Asia/Chungking #= Asia/Chongqing
313 | Link Asia/Dhaka Asia/Dacca
314 | # Istanbul is in both continents.
315 | Link Europe/Istanbul Asia/Istanbul
316 | Link Asia/Kathmandu Asia/Katmandu
317 | Link Asia/Macau Asia/Macao
318 | Link Asia/Yangon Asia/Rangoon
319 | Link Asia/Ho_Chi_Minh Asia/Saigon
320 | Link Asia/Thimphu Asia/Thimbu
321 | Link Asia/Makassar Asia/Ujung_Pandang
322 | Link Asia/Ulaanbaatar Asia/Ulan_Bator
323 | Link Atlantic/Faroe Atlantic/Faeroe
324 | Link Europe/Kyiv Europe/Kiev
325 | # Classically, Cyprus is in Asia; e.g. see Herodotus, Histories, I.72.
326 | # However, for various reasons many users expect to find it under Europe.
327 | Link Asia/Nicosia Europe/Nicosia
328 | Link Pacific/Honolulu HST
329 | Link America/Los_Angeles PST8PDT
330 | Link Pacific/Guadalcanal Pacific/Ponape #= Pacific/Pohnpei
331 | Link Pacific/Port_Moresby Pacific/Truk #= Pacific/Chuuk
332 |
--------------------------------------------------------------------------------
/priv/tzdata2024b/etcetera:
--------------------------------------------------------------------------------
1 | # tzdb data for ships at sea and other miscellany
2 |
3 | # This file is in the public domain, so clarified as of
4 | # 2009-05-17 by Arthur David Olson.
5 |
6 | # These entries are for uses not otherwise covered by the tz database.
7 | # Their main practical use is for platforms like Android that lack
8 | # support for POSIX proleptic TZ strings. On such platforms these entries
9 | # can be useful if the timezone database is wrong or if a ship or
10 | # aircraft at sea is not in a timezone.
11 |
12 | # Starting with POSIX 1003.1-2001, the entries below are all
13 | # unnecessary as settings for the TZ environment variable. E.g.,
14 | # instead of TZ='Etc/GMT+4' one can use the POSIX setting TZ='<-04>+4'.
15 | #
16 | # Do not use a POSIX TZ setting like TZ='GMT+4', which is four hours
17 | # behind GMT but uses the completely misleading abbreviation "GMT".
18 |
19 | # The following zone is used by tzcode functions like gmtime,
20 | # which load the "UTC" file to handle seconds properly.
21 | Zone Etc/UTC 0 - UTC
22 |
23 | # Functions like gmtime load the "GMT" file to handle leap seconds properly.
24 | # Vanguard section, which works with most .zi parsers.
25 | #Zone GMT 0 - GMT
26 | # Rearguard section, for TZUpdater 2.3.2 and earlier.
27 | Zone Etc/GMT 0 - GMT
28 |
29 | # The following link uses older naming conventions,
30 | # but it belongs here, not in the file 'backward',
31 | # as it is needed for tzcode releases through 2022a,
32 | # where functions like gmtime load "GMT" instead of the "Etc/UTC".
33 | # We want this to work even on installations that omit 'backward'.
34 | Link Etc/GMT GMT
35 | # End of rearguard section.
36 |
37 | # Be consistent with POSIX TZ settings in the Zone names,
38 | # even though this is the opposite of what many people expect.
39 | # POSIX has positive signs west of Greenwich, but many people expect
40 | # positive signs east of Greenwich. For example, TZ='Etc/GMT+4' uses
41 | # the abbreviation "-04" and corresponds to 4 hours behind UT
42 | # (i.e. west of Greenwich) even though many people would expect it to
43 | # mean 4 hours ahead of UT (i.e. east of Greenwich).
44 |
45 | # Earlier incarnations of this package were not POSIX-compliant,
46 | # and had lines such as
47 | # Zone GMT-12 -12 - GMT-1200
48 | # We did not want things to change quietly if someone accustomed to the old
49 | # way does a
50 | # zic -l GMT-12
51 | # so we moved the names into the Etc subdirectory.
52 | # Also, the time zone abbreviations are now compatible with %z.
53 |
54 | Zone Etc/GMT-14 14 - %z
55 | Zone Etc/GMT-13 13 - %z
56 | Zone Etc/GMT-12 12 - %z
57 | Zone Etc/GMT-11 11 - %z
58 | Zone Etc/GMT-10 10 - %z
59 | Zone Etc/GMT-9 9 - %z
60 | Zone Etc/GMT-8 8 - %z
61 | Zone Etc/GMT-7 7 - %z
62 | Zone Etc/GMT-6 6 - %z
63 | Zone Etc/GMT-5 5 - %z
64 | Zone Etc/GMT-4 4 - %z
65 | Zone Etc/GMT-3 3 - %z
66 | Zone Etc/GMT-2 2 - %z
67 | Zone Etc/GMT-1 1 - %z
68 | Zone Etc/GMT+1 -1 - %z
69 | Zone Etc/GMT+2 -2 - %z
70 | Zone Etc/GMT+3 -3 - %z
71 | Zone Etc/GMT+4 -4 - %z
72 | Zone Etc/GMT+5 -5 - %z
73 | Zone Etc/GMT+6 -6 - %z
74 | Zone Etc/GMT+7 -7 - %z
75 | Zone Etc/GMT+8 -8 - %z
76 | Zone Etc/GMT+9 -9 - %z
77 | Zone Etc/GMT+10 -10 - %z
78 | Zone Etc/GMT+11 -11 - %z
79 | Zone Etc/GMT+12 -12 - %z
80 |
--------------------------------------------------------------------------------
/priv/tzdata2024b/iso3166.tab:
--------------------------------------------------------------------------------
1 | # ISO 3166 alpha-2 country codes
2 | #
3 | # This file is in the public domain, so clarified as of
4 | # 2009-05-17 by Arthur David Olson.
5 | #
6 | # From Paul Eggert (2023-09-06):
7 | # This file contains a table of two-letter country codes. Columns are
8 | # separated by a single tab. Lines beginning with '#' are comments.
9 | # All text uses UTF-8 encoding. The columns of the table are as follows:
10 | #
11 | # 1. ISO 3166-1 alpha-2 country code, current as of
12 | # ISO/TC 46 N1108 (2023-04-05). See: ISO/TC 46 Documents
13 | # https://www.iso.org/committee/48750.html?view=documents
14 | # 2. The usual English name for the coded region. This sometimes
15 | # departs from ISO-listed names, sometimes so that sorted subsets
16 | # of names are useful (e.g., "Samoa (American)" and "Samoa
17 | # (western)" rather than "American Samoa" and "Samoa"),
18 | # sometimes to avoid confusion among non-experts (e.g.,
19 | # "Czech Republic" and "Turkey" rather than "Czechia" and "Türkiye"),
20 | # and sometimes to omit needless detail or churn (e.g., "Netherlands"
21 | # rather than "Netherlands (the)" or "Netherlands (Kingdom of the)").
22 | #
23 | # The table is sorted by country code.
24 | #
25 | # This table is intended as an aid for users, to help them select time
26 | # zone data appropriate for their practical needs. It is not intended
27 | # to take or endorse any position on legal or territorial claims.
28 | #
29 | #country-
30 | #code name of country, territory, area, or subdivision
31 | AD Andorra
32 | AE United Arab Emirates
33 | AF Afghanistan
34 | AG Antigua & Barbuda
35 | AI Anguilla
36 | AL Albania
37 | AM Armenia
38 | AO Angola
39 | AQ Antarctica
40 | AR Argentina
41 | AS Samoa (American)
42 | AT Austria
43 | AU Australia
44 | AW Aruba
45 | AX Åland Islands
46 | AZ Azerbaijan
47 | BA Bosnia & Herzegovina
48 | BB Barbados
49 | BD Bangladesh
50 | BE Belgium
51 | BF Burkina Faso
52 | BG Bulgaria
53 | BH Bahrain
54 | BI Burundi
55 | BJ Benin
56 | BL St Barthelemy
57 | BM Bermuda
58 | BN Brunei
59 | BO Bolivia
60 | BQ Caribbean NL
61 | BR Brazil
62 | BS Bahamas
63 | BT Bhutan
64 | BV Bouvet Island
65 | BW Botswana
66 | BY Belarus
67 | BZ Belize
68 | CA Canada
69 | CC Cocos (Keeling) Islands
70 | CD Congo (Dem. Rep.)
71 | CF Central African Rep.
72 | CG Congo (Rep.)
73 | CH Switzerland
74 | CI Côte d'Ivoire
75 | CK Cook Islands
76 | CL Chile
77 | CM Cameroon
78 | CN China
79 | CO Colombia
80 | CR Costa Rica
81 | CU Cuba
82 | CV Cape Verde
83 | CW Curaçao
84 | CX Christmas Island
85 | CY Cyprus
86 | CZ Czech Republic
87 | DE Germany
88 | DJ Djibouti
89 | DK Denmark
90 | DM Dominica
91 | DO Dominican Republic
92 | DZ Algeria
93 | EC Ecuador
94 | EE Estonia
95 | EG Egypt
96 | EH Western Sahara
97 | ER Eritrea
98 | ES Spain
99 | ET Ethiopia
100 | FI Finland
101 | FJ Fiji
102 | FK Falkland Islands
103 | FM Micronesia
104 | FO Faroe Islands
105 | FR France
106 | GA Gabon
107 | GB Britain (UK)
108 | GD Grenada
109 | GE Georgia
110 | GF French Guiana
111 | GG Guernsey
112 | GH Ghana
113 | GI Gibraltar
114 | GL Greenland
115 | GM Gambia
116 | GN Guinea
117 | GP Guadeloupe
118 | GQ Equatorial Guinea
119 | GR Greece
120 | GS South Georgia & the South Sandwich Islands
121 | GT Guatemala
122 | GU Guam
123 | GW Guinea-Bissau
124 | GY Guyana
125 | HK Hong Kong
126 | HM Heard Island & McDonald Islands
127 | HN Honduras
128 | HR Croatia
129 | HT Haiti
130 | HU Hungary
131 | ID Indonesia
132 | IE Ireland
133 | IL Israel
134 | IM Isle of Man
135 | IN India
136 | IO British Indian Ocean Territory
137 | IQ Iraq
138 | IR Iran
139 | IS Iceland
140 | IT Italy
141 | JE Jersey
142 | JM Jamaica
143 | JO Jordan
144 | JP Japan
145 | KE Kenya
146 | KG Kyrgyzstan
147 | KH Cambodia
148 | KI Kiribati
149 | KM Comoros
150 | KN St Kitts & Nevis
151 | KP Korea (North)
152 | KR Korea (South)
153 | KW Kuwait
154 | KY Cayman Islands
155 | KZ Kazakhstan
156 | LA Laos
157 | LB Lebanon
158 | LC St Lucia
159 | LI Liechtenstein
160 | LK Sri Lanka
161 | LR Liberia
162 | LS Lesotho
163 | LT Lithuania
164 | LU Luxembourg
165 | LV Latvia
166 | LY Libya
167 | MA Morocco
168 | MC Monaco
169 | MD Moldova
170 | ME Montenegro
171 | MF St Martin (French)
172 | MG Madagascar
173 | MH Marshall Islands
174 | MK North Macedonia
175 | ML Mali
176 | MM Myanmar (Burma)
177 | MN Mongolia
178 | MO Macau
179 | MP Northern Mariana Islands
180 | MQ Martinique
181 | MR Mauritania
182 | MS Montserrat
183 | MT Malta
184 | MU Mauritius
185 | MV Maldives
186 | MW Malawi
187 | MX Mexico
188 | MY Malaysia
189 | MZ Mozambique
190 | NA Namibia
191 | NC New Caledonia
192 | NE Niger
193 | NF Norfolk Island
194 | NG Nigeria
195 | NI Nicaragua
196 | NL Netherlands
197 | NO Norway
198 | NP Nepal
199 | NR Nauru
200 | NU Niue
201 | NZ New Zealand
202 | OM Oman
203 | PA Panama
204 | PE Peru
205 | PF French Polynesia
206 | PG Papua New Guinea
207 | PH Philippines
208 | PK Pakistan
209 | PL Poland
210 | PM St Pierre & Miquelon
211 | PN Pitcairn
212 | PR Puerto Rico
213 | PS Palestine
214 | PT Portugal
215 | PW Palau
216 | PY Paraguay
217 | QA Qatar
218 | RE Réunion
219 | RO Romania
220 | RS Serbia
221 | RU Russia
222 | RW Rwanda
223 | SA Saudi Arabia
224 | SB Solomon Islands
225 | SC Seychelles
226 | SD Sudan
227 | SE Sweden
228 | SG Singapore
229 | SH St Helena
230 | SI Slovenia
231 | SJ Svalbard & Jan Mayen
232 | SK Slovakia
233 | SL Sierra Leone
234 | SM San Marino
235 | SN Senegal
236 | SO Somalia
237 | SR Suriname
238 | SS South Sudan
239 | ST Sao Tome & Principe
240 | SV El Salvador
241 | SX St Maarten (Dutch)
242 | SY Syria
243 | SZ Eswatini (Swaziland)
244 | TC Turks & Caicos Is
245 | TD Chad
246 | TF French S. Terr.
247 | TG Togo
248 | TH Thailand
249 | TJ Tajikistan
250 | TK Tokelau
251 | TL East Timor
252 | TM Turkmenistan
253 | TN Tunisia
254 | TO Tonga
255 | TR Turkey
256 | TT Trinidad & Tobago
257 | TV Tuvalu
258 | TW Taiwan
259 | TZ Tanzania
260 | UA Ukraine
261 | UG Uganda
262 | UM US minor outlying islands
263 | US United States
264 | UY Uruguay
265 | UZ Uzbekistan
266 | VA Vatican City
267 | VC St Vincent
268 | VE Venezuela
269 | VG Virgin Islands (UK)
270 | VI Virgin Islands (US)
271 | VN Vietnam
272 | VU Vanuatu
273 | WF Wallis & Futuna
274 | WS Samoa (western)
275 | YE Yemen
276 | YT Mayotte
277 | ZA South Africa
278 | ZM Zambia
279 | ZW Zimbabwe
280 |
--------------------------------------------------------------------------------
/priv/tzdata2024b/zone1970.tab:
--------------------------------------------------------------------------------
1 | # tzdb timezone descriptions
2 | #
3 | # This file is in the public domain.
4 | #
5 | # From Paul Eggert (2018-06-27):
6 | # This file contains a table where each row stands for a timezone where
7 | # civil timestamps have agreed since 1970. Columns are separated by
8 | # a single tab. Lines beginning with '#' are comments. All text uses
9 | # UTF-8 encoding. The columns of the table are as follows:
10 | #
11 | # 1. The countries that overlap the timezone, as a comma-separated list
12 | # of ISO 3166 2-character country codes. See the file 'iso3166.tab'.
13 | # 2. Latitude and longitude of the timezone's principal location
14 | # in ISO 6709 sign-degrees-minutes-seconds format,
15 | # either ±DDMM±DDDMM or ±DDMMSS±DDDMMSS,
16 | # first latitude (+ is north), then longitude (+ is east).
17 | # 3. Timezone name used in value of TZ environment variable.
18 | # Please see the theory.html file for how these names are chosen.
19 | # If multiple timezones overlap a country, each has a row in the
20 | # table, with each column 1 containing the country code.
21 | # 4. Comments; present if and only if countries have multiple timezones,
22 | # and useful only for those countries. For example, the comments
23 | # for the row with countries CH,DE,LI and name Europe/Zurich
24 | # are useful only for DE, since CH and LI have no other timezones.
25 | #
26 | # If a timezone covers multiple countries, the most-populous city is used,
27 | # and that country is listed first in column 1; any other countries
28 | # are listed alphabetically by country code. The table is sorted
29 | # first by country code, then (if possible) by an order within the
30 | # country that (1) makes some geographical sense, and (2) puts the
31 | # most populous timezones first, where that does not contradict (1).
32 | #
33 | # This table is intended as an aid for users, to help them select timezones
34 | # appropriate for their practical needs. It is not intended to take or
35 | # endorse any position on legal or territorial claims.
36 | #
37 | #country-
38 | #codes coordinates TZ comments
39 | AD +4230+00131 Europe/Andorra
40 | AE,OM,RE,SC,TF +2518+05518 Asia/Dubai Crozet
41 | AF +3431+06912 Asia/Kabul
42 | AL +4120+01950 Europe/Tirane
43 | AM +4011+04430 Asia/Yerevan
44 | AQ -6617+11031 Antarctica/Casey Casey
45 | AQ -6835+07758 Antarctica/Davis Davis
46 | AQ -6736+06253 Antarctica/Mawson Mawson
47 | AQ -6448-06406 Antarctica/Palmer Palmer
48 | AQ -6734-06808 Antarctica/Rothera Rothera
49 | AQ -720041+0023206 Antarctica/Troll Troll
50 | AQ -7824+10654 Antarctica/Vostok Vostok
51 | AR -3436-05827 America/Argentina/Buenos_Aires Buenos Aires (BA, CF)
52 | AR -3124-06411 America/Argentina/Cordoba most areas: CB, CC, CN, ER, FM, MN, SE, SF
53 | AR -2447-06525 America/Argentina/Salta Salta (SA, LP, NQ, RN)
54 | AR -2411-06518 America/Argentina/Jujuy Jujuy (JY)
55 | AR -2649-06513 America/Argentina/Tucuman Tucumán (TM)
56 | AR -2828-06547 America/Argentina/Catamarca Catamarca (CT), Chubut (CH)
57 | AR -2926-06651 America/Argentina/La_Rioja La Rioja (LR)
58 | AR -3132-06831 America/Argentina/San_Juan San Juan (SJ)
59 | AR -3253-06849 America/Argentina/Mendoza Mendoza (MZ)
60 | AR -3319-06621 America/Argentina/San_Luis San Luis (SL)
61 | AR -5138-06913 America/Argentina/Rio_Gallegos Santa Cruz (SC)
62 | AR -5448-06818 America/Argentina/Ushuaia Tierra del Fuego (TF)
63 | AS,UM -1416-17042 Pacific/Pago_Pago Midway
64 | AT +4813+01620 Europe/Vienna
65 | AU -3133+15905 Australia/Lord_Howe Lord Howe Island
66 | AU -5430+15857 Antarctica/Macquarie Macquarie Island
67 | AU -4253+14719 Australia/Hobart Tasmania
68 | AU -3749+14458 Australia/Melbourne Victoria
69 | AU -3352+15113 Australia/Sydney New South Wales (most areas)
70 | AU -3157+14127 Australia/Broken_Hill New South Wales (Yancowinna)
71 | AU -2728+15302 Australia/Brisbane Queensland (most areas)
72 | AU -2016+14900 Australia/Lindeman Queensland (Whitsunday Islands)
73 | AU -3455+13835 Australia/Adelaide South Australia
74 | AU -1228+13050 Australia/Darwin Northern Territory
75 | AU -3157+11551 Australia/Perth Western Australia (most areas)
76 | AU -3143+12852 Australia/Eucla Western Australia (Eucla)
77 | AZ +4023+04951 Asia/Baku
78 | BB +1306-05937 America/Barbados
79 | BD +2343+09025 Asia/Dhaka
80 | BE,LU,NL +5050+00420 Europe/Brussels
81 | BG +4241+02319 Europe/Sofia
82 | BM +3217-06446 Atlantic/Bermuda
83 | BO -1630-06809 America/La_Paz
84 | BR -0351-03225 America/Noronha Atlantic islands
85 | BR -0127-04829 America/Belem Pará (east), Amapá
86 | BR -0343-03830 America/Fortaleza Brazil (northeast: MA, PI, CE, RN, PB)
87 | BR -0803-03454 America/Recife Pernambuco
88 | BR -0712-04812 America/Araguaina Tocantins
89 | BR -0940-03543 America/Maceio Alagoas, Sergipe
90 | BR -1259-03831 America/Bahia Bahia
91 | BR -2332-04637 America/Sao_Paulo Brazil (southeast: GO, DF, MG, ES, RJ, SP, PR, SC, RS)
92 | BR -2027-05437 America/Campo_Grande Mato Grosso do Sul
93 | BR -1535-05605 America/Cuiaba Mato Grosso
94 | BR -0226-05452 America/Santarem Pará (west)
95 | BR -0846-06354 America/Porto_Velho Rondônia
96 | BR +0249-06040 America/Boa_Vista Roraima
97 | BR -0308-06001 America/Manaus Amazonas (east)
98 | BR -0640-06952 America/Eirunepe Amazonas (west)
99 | BR -0958-06748 America/Rio_Branco Acre
100 | BT +2728+08939 Asia/Thimphu
101 | BY +5354+02734 Europe/Minsk
102 | BZ +1730-08812 America/Belize
103 | CA +4734-05243 America/St_Johns Newfoundland, Labrador (SE)
104 | CA +4439-06336 America/Halifax Atlantic - NS (most areas), PE
105 | CA +4612-05957 America/Glace_Bay Atlantic - NS (Cape Breton)
106 | CA +4606-06447 America/Moncton Atlantic - New Brunswick
107 | CA +5320-06025 America/Goose_Bay Atlantic - Labrador (most areas)
108 | CA,BS +4339-07923 America/Toronto Eastern - ON & QC (most areas)
109 | CA +6344-06828 America/Iqaluit Eastern - NU (most areas)
110 | CA +4953-09709 America/Winnipeg Central - ON (west), Manitoba
111 | CA +744144-0944945 America/Resolute Central - NU (Resolute)
112 | CA +624900-0920459 America/Rankin_Inlet Central - NU (central)
113 | CA +5024-10439 America/Regina CST - SK (most areas)
114 | CA +5017-10750 America/Swift_Current CST - SK (midwest)
115 | CA +5333-11328 America/Edmonton Mountain - AB, BC(E), NT(E), SK(W)
116 | CA +690650-1050310 America/Cambridge_Bay Mountain - NU (west)
117 | CA +682059-1334300 America/Inuvik Mountain - NT (west)
118 | CA +5546-12014 America/Dawson_Creek MST - BC (Dawson Cr, Ft St John)
119 | CA +5848-12242 America/Fort_Nelson MST - BC (Ft Nelson)
120 | CA +6043-13503 America/Whitehorse MST - Yukon (east)
121 | CA +6404-13925 America/Dawson MST - Yukon (west)
122 | CA +4916-12307 America/Vancouver Pacific - BC (most areas)
123 | CH,DE,LI +4723+00832 Europe/Zurich Büsingen
124 | CI,BF,GH,GM,GN,IS,ML,MR,SH,SL,SN,TG +0519-00402 Africa/Abidjan
125 | CK -2114-15946 Pacific/Rarotonga
126 | CL -3327-07040 America/Santiago most of Chile
127 | CL -5309-07055 America/Punta_Arenas Region of Magallanes
128 | CL -2709-10926 Pacific/Easter Easter Island
129 | CN +3114+12128 Asia/Shanghai Beijing Time
130 | CN +4348+08735 Asia/Urumqi Xinjiang Time
131 | CO +0436-07405 America/Bogota
132 | CR +0956-08405 America/Costa_Rica
133 | CU +2308-08222 America/Havana
134 | CV +1455-02331 Atlantic/Cape_Verde
135 | CY +3510+03322 Asia/Nicosia most of Cyprus
136 | CY +3507+03357 Asia/Famagusta Northern Cyprus
137 | CZ,SK +5005+01426 Europe/Prague
138 | DE,DK,NO,SE,SJ +5230+01322 Europe/Berlin most of Germany
139 | DO +1828-06954 America/Santo_Domingo
140 | DZ +3647+00303 Africa/Algiers
141 | EC -0210-07950 America/Guayaquil Ecuador (mainland)
142 | EC -0054-08936 Pacific/Galapagos Galápagos Islands
143 | EE +5925+02445 Europe/Tallinn
144 | EG +3003+03115 Africa/Cairo
145 | EH +2709-01312 Africa/El_Aaiun
146 | ES +4024-00341 Europe/Madrid Spain (mainland)
147 | ES +3553-00519 Africa/Ceuta Ceuta, Melilla
148 | ES +2806-01524 Atlantic/Canary Canary Islands
149 | FI,AX +6010+02458 Europe/Helsinki
150 | FJ -1808+17825 Pacific/Fiji
151 | FK -5142-05751 Atlantic/Stanley
152 | FM +0519+16259 Pacific/Kosrae Kosrae
153 | FO +6201-00646 Atlantic/Faroe
154 | FR,MC +4852+00220 Europe/Paris
155 | GB,GG,IM,JE +513030-0000731 Europe/London
156 | GE +4143+04449 Asia/Tbilisi
157 | GF +0456-05220 America/Cayenne
158 | GI +3608-00521 Europe/Gibraltar
159 | GL +6411-05144 America/Nuuk most of Greenland
160 | GL +7646-01840 America/Danmarkshavn National Park (east coast)
161 | GL +7029-02158 America/Scoresbysund Scoresbysund/Ittoqqortoormiit
162 | GL +7634-06847 America/Thule Thule/Pituffik
163 | GR +3758+02343 Europe/Athens
164 | GS -5416-03632 Atlantic/South_Georgia
165 | GT +1438-09031 America/Guatemala
166 | GU,MP +1328+14445 Pacific/Guam
167 | GW +1151-01535 Africa/Bissau
168 | GY +0648-05810 America/Guyana
169 | HK +2217+11409 Asia/Hong_Kong
170 | HN +1406-08713 America/Tegucigalpa
171 | HT +1832-07220 America/Port-au-Prince
172 | HU +4730+01905 Europe/Budapest
173 | ID -0610+10648 Asia/Jakarta Java, Sumatra
174 | ID -0002+10920 Asia/Pontianak Borneo (west, central)
175 | ID -0507+11924 Asia/Makassar Borneo (east, south), Sulawesi/Celebes, Bali, Nusa Tengarra, Timor (west)
176 | ID -0232+14042 Asia/Jayapura New Guinea (West Papua / Irian Jaya), Malukus/Moluccas
177 | IE +5320-00615 Europe/Dublin
178 | IL +314650+0351326 Asia/Jerusalem
179 | IN +2232+08822 Asia/Kolkata
180 | IO -0720+07225 Indian/Chagos
181 | IQ +3321+04425 Asia/Baghdad
182 | IR +3540+05126 Asia/Tehran
183 | IT,SM,VA +4154+01229 Europe/Rome
184 | JM +175805-0764736 America/Jamaica
185 | JO +3157+03556 Asia/Amman
186 | JP +353916+1394441 Asia/Tokyo
187 | KE,DJ,ER,ET,KM,MG,SO,TZ,UG,YT -0117+03649 Africa/Nairobi
188 | KG +4254+07436 Asia/Bishkek
189 | KI,MH,TV,UM,WF +0125+17300 Pacific/Tarawa Gilberts, Marshalls, Wake
190 | KI -0247-17143 Pacific/Kanton Phoenix Islands
191 | KI +0152-15720 Pacific/Kiritimati Line Islands
192 | KP +3901+12545 Asia/Pyongyang
193 | KR +3733+12658 Asia/Seoul
194 | KZ +4315+07657 Asia/Almaty most of Kazakhstan
195 | KZ +4448+06528 Asia/Qyzylorda Qyzylorda/Kyzylorda/Kzyl-Orda
196 | KZ +5312+06337 Asia/Qostanay Qostanay/Kostanay/Kustanay
197 | KZ +5017+05710 Asia/Aqtobe Aqtöbe/Aktobe
198 | KZ +4431+05016 Asia/Aqtau Mangghystaū/Mankistau
199 | KZ +4707+05156 Asia/Atyrau Atyraū/Atirau/Gur'yev
200 | KZ +5113+05121 Asia/Oral West Kazakhstan
201 | LB +3353+03530 Asia/Beirut
202 | LK +0656+07951 Asia/Colombo
203 | LR +0618-01047 Africa/Monrovia
204 | LT +5441+02519 Europe/Vilnius
205 | LV +5657+02406 Europe/Riga
206 | LY +3254+01311 Africa/Tripoli
207 | MA +3339-00735 Africa/Casablanca
208 | MD +4700+02850 Europe/Chisinau
209 | MH +0905+16720 Pacific/Kwajalein Kwajalein
210 | MM,CC +1647+09610 Asia/Yangon
211 | MN +4755+10653 Asia/Ulaanbaatar most of Mongolia
212 | MN +4801+09139 Asia/Hovd Bayan-Ölgii, Hovd, Uvs
213 | MO +221150+1133230 Asia/Macau
214 | MQ +1436-06105 America/Martinique
215 | MT +3554+01431 Europe/Malta
216 | MU -2010+05730 Indian/Mauritius
217 | MV,TF +0410+07330 Indian/Maldives Kerguelen, St Paul I, Amsterdam I
218 | MX +1924-09909 America/Mexico_City Central Mexico
219 | MX +2105-08646 America/Cancun Quintana Roo
220 | MX +2058-08937 America/Merida Campeche, Yucatán
221 | MX +2540-10019 America/Monterrey Durango; Coahuila, Nuevo León, Tamaulipas (most areas)
222 | MX +2550-09730 America/Matamoros Coahuila, Nuevo León, Tamaulipas (US border)
223 | MX +2838-10605 America/Chihuahua Chihuahua (most areas)
224 | MX +3144-10629 America/Ciudad_Juarez Chihuahua (US border - west)
225 | MX +2934-10425 America/Ojinaga Chihuahua (US border - east)
226 | MX +2313-10625 America/Mazatlan Baja California Sur, Nayarit (most areas), Sinaloa
227 | MX +2048-10515 America/Bahia_Banderas Bahía de Banderas
228 | MX +2904-11058 America/Hermosillo Sonora
229 | MX +3232-11701 America/Tijuana Baja California
230 | MY,BN +0133+11020 Asia/Kuching Sabah, Sarawak
231 | MZ,BI,BW,CD,MW,RW,ZM,ZW -2558+03235 Africa/Maputo Central Africa Time
232 | NA -2234+01706 Africa/Windhoek
233 | NC -2216+16627 Pacific/Noumea
234 | NF -2903+16758 Pacific/Norfolk
235 | NG,AO,BJ,CD,CF,CG,CM,GA,GQ,NE +0627+00324 Africa/Lagos West Africa Time
236 | NI +1209-08617 America/Managua
237 | NP +2743+08519 Asia/Kathmandu
238 | NR -0031+16655 Pacific/Nauru
239 | NU -1901-16955 Pacific/Niue
240 | NZ,AQ -3652+17446 Pacific/Auckland New Zealand time
241 | NZ -4357-17633 Pacific/Chatham Chatham Islands
242 | PA,CA,KY +0858-07932 America/Panama EST - ON (Atikokan), NU (Coral H)
243 | PE -1203-07703 America/Lima
244 | PF -1732-14934 Pacific/Tahiti Society Islands
245 | PF -0900-13930 Pacific/Marquesas Marquesas Islands
246 | PF -2308-13457 Pacific/Gambier Gambier Islands
247 | PG,AQ,FM -0930+14710 Pacific/Port_Moresby Papua New Guinea (most areas), Chuuk, Yap, Dumont d'Urville
248 | PG -0613+15534 Pacific/Bougainville Bougainville
249 | PH +1435+12100 Asia/Manila
250 | PK +2452+06703 Asia/Karachi
251 | PL +5215+02100 Europe/Warsaw
252 | PM +4703-05620 America/Miquelon
253 | PN -2504-13005 Pacific/Pitcairn
254 | PR,AG,CA,AI,AW,BL,BQ,CW,DM,GD,GP,KN,LC,MF,MS,SX,TT,VC,VG,VI +182806-0660622 America/Puerto_Rico AST - QC (Lower North Shore)
255 | PS +3130+03428 Asia/Gaza Gaza Strip
256 | PS +313200+0350542 Asia/Hebron West Bank
257 | PT +3843-00908 Europe/Lisbon Portugal (mainland)
258 | PT +3238-01654 Atlantic/Madeira Madeira Islands
259 | PT +3744-02540 Atlantic/Azores Azores
260 | PW +0720+13429 Pacific/Palau
261 | PY -2516-05740 America/Asuncion
262 | QA,BH +2517+05132 Asia/Qatar
263 | RO +4426+02606 Europe/Bucharest
264 | RS,BA,HR,ME,MK,SI +4450+02030 Europe/Belgrade
265 | RU +5443+02030 Europe/Kaliningrad MSK-01 - Kaliningrad
266 | RU +554521+0373704 Europe/Moscow MSK+00 - Moscow area
267 | # Mention RU and UA alphabetically. See "territorial claims" above.
268 | RU,UA +4457+03406 Europe/Simferopol Crimea
269 | RU +5836+04939 Europe/Kirov MSK+00 - Kirov
270 | RU +4844+04425 Europe/Volgograd MSK+00 - Volgograd
271 | RU +4621+04803 Europe/Astrakhan MSK+01 - Astrakhan
272 | RU +5134+04602 Europe/Saratov MSK+01 - Saratov
273 | RU +5420+04824 Europe/Ulyanovsk MSK+01 - Ulyanovsk
274 | RU +5312+05009 Europe/Samara MSK+01 - Samara, Udmurtia
275 | RU +5651+06036 Asia/Yekaterinburg MSK+02 - Urals
276 | RU +5500+07324 Asia/Omsk MSK+03 - Omsk
277 | RU +5502+08255 Asia/Novosibirsk MSK+04 - Novosibirsk
278 | RU +5322+08345 Asia/Barnaul MSK+04 - Altai
279 | RU +5630+08458 Asia/Tomsk MSK+04 - Tomsk
280 | RU +5345+08707 Asia/Novokuznetsk MSK+04 - Kemerovo
281 | RU +5601+09250 Asia/Krasnoyarsk MSK+04 - Krasnoyarsk area
282 | RU +5216+10420 Asia/Irkutsk MSK+05 - Irkutsk, Buryatia
283 | RU +5203+11328 Asia/Chita MSK+06 - Zabaykalsky
284 | RU +6200+12940 Asia/Yakutsk MSK+06 - Lena River
285 | RU +623923+1353314 Asia/Khandyga MSK+06 - Tomponsky, Ust-Maysky
286 | RU +4310+13156 Asia/Vladivostok MSK+07 - Amur River
287 | RU +643337+1431336 Asia/Ust-Nera MSK+07 - Oymyakonsky
288 | RU +5934+15048 Asia/Magadan MSK+08 - Magadan
289 | RU +4658+14242 Asia/Sakhalin MSK+08 - Sakhalin Island
290 | RU +6728+15343 Asia/Srednekolymsk MSK+08 - Sakha (E), N Kuril Is
291 | RU +5301+15839 Asia/Kamchatka MSK+09 - Kamchatka
292 | RU +6445+17729 Asia/Anadyr MSK+09 - Bering Sea
293 | SA,AQ,KW,YE +2438+04643 Asia/Riyadh Syowa
294 | SB,FM -0932+16012 Pacific/Guadalcanal Pohnpei
295 | SD +1536+03232 Africa/Khartoum
296 | SG,MY +0117+10351 Asia/Singapore peninsular Malaysia
297 | SR +0550-05510 America/Paramaribo
298 | SS +0451+03137 Africa/Juba
299 | ST +0020+00644 Africa/Sao_Tome
300 | SV +1342-08912 America/El_Salvador
301 | SY +3330+03618 Asia/Damascus
302 | TC +2128-07108 America/Grand_Turk
303 | TD +1207+01503 Africa/Ndjamena
304 | TH,CX,KH,LA,VN +1345+10031 Asia/Bangkok north Vietnam
305 | TJ +3835+06848 Asia/Dushanbe
306 | TK -0922-17114 Pacific/Fakaofo
307 | TL -0833+12535 Asia/Dili
308 | TM +3757+05823 Asia/Ashgabat
309 | TN +3648+01011 Africa/Tunis
310 | TO -210800-1751200 Pacific/Tongatapu
311 | TR +4101+02858 Europe/Istanbul
312 | TW +2503+12130 Asia/Taipei
313 | UA +5026+03031 Europe/Kyiv most of Ukraine
314 | US +404251-0740023 America/New_York Eastern (most areas)
315 | US +421953-0830245 America/Detroit Eastern - MI (most areas)
316 | US +381515-0854534 America/Kentucky/Louisville Eastern - KY (Louisville area)
317 | US +364947-0845057 America/Kentucky/Monticello Eastern - KY (Wayne)
318 | US +394606-0860929 America/Indiana/Indianapolis Eastern - IN (most areas)
319 | US +384038-0873143 America/Indiana/Vincennes Eastern - IN (Da, Du, K, Mn)
320 | US +410305-0863611 America/Indiana/Winamac Eastern - IN (Pulaski)
321 | US +382232-0862041 America/Indiana/Marengo Eastern - IN (Crawford)
322 | US +382931-0871643 America/Indiana/Petersburg Eastern - IN (Pike)
323 | US +384452-0850402 America/Indiana/Vevay Eastern - IN (Switzerland)
324 | US +415100-0873900 America/Chicago Central (most areas)
325 | US +375711-0864541 America/Indiana/Tell_City Central - IN (Perry)
326 | US +411745-0863730 America/Indiana/Knox Central - IN (Starke)
327 | US +450628-0873651 America/Menominee Central - MI (Wisconsin border)
328 | US +470659-1011757 America/North_Dakota/Center Central - ND (Oliver)
329 | US +465042-1012439 America/North_Dakota/New_Salem Central - ND (Morton rural)
330 | US +471551-1014640 America/North_Dakota/Beulah Central - ND (Mercer)
331 | US +394421-1045903 America/Denver Mountain (most areas)
332 | US +433649-1161209 America/Boise Mountain - ID (south), OR (east)
333 | US,CA +332654-1120424 America/Phoenix MST - AZ (most areas), Creston BC
334 | US +340308-1181434 America/Los_Angeles Pacific
335 | US +611305-1495401 America/Anchorage Alaska (most areas)
336 | US +581807-1342511 America/Juneau Alaska - Juneau area
337 | US +571035-1351807 America/Sitka Alaska - Sitka area
338 | US +550737-1313435 America/Metlakatla Alaska - Annette Island
339 | US +593249-1394338 America/Yakutat Alaska - Yakutat
340 | US +643004-1652423 America/Nome Alaska (west)
341 | US +515248-1763929 America/Adak Alaska - western Aleutians
342 | US +211825-1575130 Pacific/Honolulu Hawaii
343 | UY -345433-0561245 America/Montevideo
344 | UZ +3940+06648 Asia/Samarkand Uzbekistan (west)
345 | UZ +4120+06918 Asia/Tashkent Uzbekistan (east)
346 | VE +1030-06656 America/Caracas
347 | VN +1045+10640 Asia/Ho_Chi_Minh south Vietnam
348 | VU -1740+16825 Pacific/Efate
349 | WS -1350-17144 Pacific/Apia
350 | ZA,LS,SZ -2615+02800 Africa/Johannesburg
351 | #
352 | # The next section contains experimental tab-separated comments for
353 | # use by user agents like tzselect that identify continents and oceans.
354 | #
355 | # For example, the comment "#@AQAntarctica/" means the country code
356 | # AQ is in the continent Antarctica regardless of the Zone name,
357 | # so Pacific/Auckland should be listed under Antarctica as well as
358 | # under the Pacific because its line's country codes include AQ.
359 | #
360 | # If more than one country code is affected each is listed separated
361 | # by commas, e.g., #@IS,SHAtlantic/". If a country code is in
362 | # more than one continent or ocean, each is listed separated by
363 | # commas, e.g., the second column of "#@CY,TRAsia/,Europe/".
364 | #
365 | # These experimental comments are present only for country codes where
366 | # the continent or ocean is not already obvious from the Zone name.
367 | # For example, there is no such comment for RU since it already
368 | # corresponds to Zone names starting with both "Europe/" and "Asia/".
369 | #
370 | #@AQ Antarctica/
371 | #@IS,SH Atlantic/
372 | #@CY,TR Asia/,Europe/
373 | #@SJ Arctic/
374 | #@CC,CX,KM,MG,YT Indian/
375 |
--------------------------------------------------------------------------------
/test/dynamic_periods_test.exs:
--------------------------------------------------------------------------------
1 | defmodule DynamicPeriodsTest do
2 | use ExUnit.Case
3 |
4 | test "dynamic periods" do
5 | time_zone = "Antarctica/Troll"
6 |
7 | {:ok, datetime} =
8 | DateTime.from_naive!(~N[2050-05-14 07:30:00], "Etc/UTC", Tz.TimeZoneDatabase)
9 | |> DateTime.shift_zone(time_zone, Tz.TimeZoneDatabase)
10 |
11 | assert DateTime.to_iso8601(datetime) == "2050-05-14T09:30:00+02:00"
12 | assert datetime.time_zone == time_zone
13 | assert datetime.zone_abbr == "+02"
14 |
15 | {:ok, datetime} =
16 | DateTime.from_naive!(~N[2050-12-14 07:30:00], "Etc/UTC", Tz.TimeZoneDatabase)
17 | |> DateTime.shift_zone(time_zone, Tz.TimeZoneDatabase)
18 |
19 | assert DateTime.to_iso8601(datetime) == "2050-12-14T07:30:00+00:00"
20 | assert datetime.time_zone == time_zone
21 | assert datetime.zone_abbr == "+00"
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/test/support/holocene_calendar.ex:
--------------------------------------------------------------------------------
1 | defmodule Support.HoloceneCalendar do
2 | # This calendar is used to test conversions between calendars.
3 | # It implements the Holocene calendar, which is based on the
4 | # Proleptic Gregorian calendar with every year + 10000.
5 |
6 | @behaviour Calendar
7 |
8 | def date(year, month, day) do
9 | %Date{year: year, month: month, day: day, calendar: __MODULE__}
10 | end
11 |
12 | def naive_datetime(year, month, day, hour, minute, second, microsecond \\ {0, 0}) do
13 | %NaiveDateTime{
14 | year: year,
15 | month: month,
16 | day: day,
17 | hour: hour,
18 | minute: minute,
19 | second: second,
20 | microsecond: microsecond,
21 | calendar: __MODULE__
22 | }
23 | end
24 |
25 | @impl true
26 | def date_to_string(year, month, day) do
27 | "#{year}-#{zero_pad(month, 2)}-#{zero_pad(day, 2)}"
28 | end
29 |
30 | @impl true
31 | def naive_datetime_to_string(year, month, day, hour, minute, second, microsecond) do
32 | "#{year}-#{zero_pad(month, 2)}-#{zero_pad(day, 2)}" <>
33 | Calendar.ISO.time_to_string(hour, minute, second, microsecond)
34 | end
35 |
36 | @impl true
37 | def datetime_to_string(
38 | year,
39 | month,
40 | day,
41 | hour,
42 | minute,
43 | second,
44 | microsecond,
45 | _time_zone,
46 | zone_abbr,
47 | _utc_offset,
48 | _std_offset
49 | ) do
50 | "#{year}-#{zero_pad(month, 2)}-#{zero_pad(day, 2)}" <>
51 | Calendar.ISO.time_to_string(hour, minute, second, microsecond) <>
52 | " #{zone_abbr}"
53 | end
54 |
55 | @impl true
56 | defdelegate time_to_string(hour, minute, second, microsecond), to: Calendar.ISO
57 |
58 | @impl true
59 | def day_rollover_relative_to_midnight_utc(), do: {0, 1}
60 |
61 | @impl true
62 | def naive_datetime_from_iso_days(entry) do
63 | {year, month, day, hour, minute, second, microsecond} =
64 | Calendar.ISO.naive_datetime_from_iso_days(entry)
65 |
66 | {year + 10000, month, day, hour, minute, second, microsecond}
67 | end
68 |
69 | @impl true
70 | def naive_datetime_to_iso_days(year, month, day, hour, minute, second, microsecond) do
71 | Calendar.ISO.naive_datetime_to_iso_days(
72 | year - 10000,
73 | month,
74 | day,
75 | hour,
76 | minute,
77 | second,
78 | microsecond
79 | )
80 | end
81 |
82 | defp zero_pad(val, count) when val >= 0 do
83 | String.pad_leading("#{val}", count, ["0"])
84 | end
85 |
86 | defp zero_pad(val, count) do
87 | "-" <> zero_pad(-val, count)
88 | end
89 |
90 | @impl true
91 | def parse_date(string) do
92 | {year, month, day} =
93 | string
94 | |> String.split("-")
95 | |> Enum.map(&String.to_integer/1)
96 | |> List.to_tuple()
97 |
98 | if valid_date?(year, month, day) do
99 | {:ok, {year, month, day}}
100 | else
101 | {:error, :invalid_date}
102 | end
103 | end
104 |
105 | @impl true
106 | def valid_date?(year, month, day) do
107 | :calendar.valid_date(year, month, day)
108 | end
109 |
110 | @impl true
111 | defdelegate parse_time(string), to: Calendar.ISO
112 |
113 | @impl true
114 | defdelegate parse_naive_datetime(string), to: Calendar.ISO
115 |
116 | @impl true
117 | defdelegate parse_utc_datetime(string), to: Calendar.ISO
118 |
119 | @impl true
120 | defdelegate time_from_day_fraction(day_fraction), to: Calendar.ISO
121 |
122 | @impl true
123 | defdelegate time_to_day_fraction(hour, minute, second, microsecond), to: Calendar.ISO
124 |
125 | @impl true
126 | defdelegate leap_year?(year), to: Calendar.ISO
127 |
128 | @impl true
129 | defdelegate days_in_month(year, month), to: Calendar.ISO
130 |
131 | @impl true
132 | defdelegate months_in_year(year), to: Calendar.ISO
133 |
134 | @impl true
135 | defdelegate day_of_week(year, month, day, starting_on), to: Calendar.ISO
136 |
137 | @impl true
138 | defdelegate day_of_year(year, month, day), to: Calendar.ISO
139 |
140 | @impl true
141 | defdelegate quarter_of_year(year, month, day), to: Calendar.ISO
142 |
143 | @impl true
144 | defdelegate year_of_era(year, month, day), to: Calendar.ISO
145 |
146 | @impl true
147 | defdelegate day_of_era(year, month, day), to: Calendar.ISO
148 |
149 | @impl true
150 | defdelegate valid_time?(hour, minute, second, microsecond), to: Calendar.ISO
151 |
152 | if Code.ensure_loaded?(Calendar.ISO) do
153 | if function_exported?(Calendar.ISO, :shift_date, 4) do
154 | @impl true
155 | defdelegate shift_date(year, month, day, t), to: Calendar.ISO
156 | end
157 |
158 | if function_exported?(Calendar.ISO, :shift_time, 5) do
159 | @impl true
160 | defdelegate shift_time(hour, minute, second, microsecond, t),
161 | to: Calendar.ISO
162 | end
163 |
164 | if function_exported?(Calendar.ISO, :shift_naive_datetime, 8) do
165 | @impl true
166 | defdelegate shift_naive_datetime(year, month, day, hour, minute, second, microsecond, t),
167 | to: Calendar.ISO
168 | end
169 |
170 | if function_exported?(Calendar.ISO, :iso_days_to_beginning_of_day, 1) do
171 | @impl true
172 | defdelegate iso_days_to_beginning_of_day(date), to: Calendar.ISO
173 | end
174 |
175 | if function_exported?(Calendar.ISO, :iso_days_to_end_of_day, 1) do
176 | @impl true
177 | defdelegate iso_days_to_end_of_day(date), to: Calendar.ISO
178 | end
179 | end
180 | end
181 |
--------------------------------------------------------------------------------
/test/test_helper.exs:
--------------------------------------------------------------------------------
1 | ExUnit.start()
2 |
--------------------------------------------------------------------------------
/test/time_zone_database_test.exs:
--------------------------------------------------------------------------------
1 | defmodule TimeZoneDatabaseTest do
2 | use ExUnit.Case
3 |
4 | alias Support.HoloceneCalendar
5 |
6 | test "naive date for time zone" do
7 | naive_date_time = ~N[2018-07-28 12:30:00]
8 | time_zone = "Europe/Copenhagen"
9 |
10 | result = DateTime.from_naive(naive_date_time, time_zone, Tz.TimeZoneDatabase)
11 |
12 | assert {:ok, datetime} = result
13 |
14 | assert DateTime.to_iso8601(datetime) == "2018-07-28T12:30:00+02:00"
15 | assert datetime.time_zone == "Europe/Copenhagen"
16 | assert datetime.zone_abbr == "CEST"
17 | end
18 |
19 | test "time zone link" do
20 | naive_date_time = ~N[2018-07-28 12:30:00]
21 | time_zone = "Europe/Mariehamn"
22 |
23 | result = DateTime.from_naive(naive_date_time, time_zone, Tz.TimeZoneDatabase)
24 |
25 | assert {:ok, datetime} = result
26 |
27 | assert datetime.time_zone == "Europe/Mariehamn"
28 | end
29 |
30 | test "naive date is ambiguous date for time zone" do
31 | naive_date_time = ~N[2018-10-28 02:30:00]
32 | time_zone = "Europe/Copenhagen"
33 |
34 | result = DateTime.from_naive(naive_date_time, time_zone, Tz.TimeZoneDatabase)
35 |
36 | assert {:ambiguous, first_dt, second_dt} = result
37 |
38 | assert DateTime.to_iso8601(first_dt) == "2018-10-28T02:30:00+02:00"
39 | assert DateTime.to_iso8601(second_dt) == "2018-10-28T02:30:00+01:00"
40 | assert first_dt.time_zone == "Europe/Copenhagen"
41 | assert first_dt.zone_abbr == "CEST"
42 | assert second_dt.time_zone == "Europe/Copenhagen"
43 | assert second_dt.zone_abbr == "CET"
44 | end
45 |
46 | test "naive date date is in gap for time zone" do
47 | naive_date_time = ~N[2019-03-31 02:30:00]
48 | time_zone = "Europe/Copenhagen"
49 |
50 | result = DateTime.from_naive(naive_date_time, time_zone, Tz.TimeZoneDatabase)
51 |
52 | assert {:gap, just_before, just_after} = result
53 |
54 | assert DateTime.to_iso8601(just_before) == "2019-03-31T01:59:59.999999+01:00"
55 | assert DateTime.to_iso8601(just_after) == "2019-03-31T03:00:00+02:00"
56 | assert just_before.time_zone == "Europe/Copenhagen"
57 | assert just_before.zone_abbr == "CET"
58 | assert just_after.time_zone == "Europe/Copenhagen"
59 | assert just_after.zone_abbr == "CEST"
60 | end
61 |
62 | test "shift UTC date to other time zone" do
63 | utc_date_time = ~U[2018-07-16 10:00:00Z]
64 | time_zone = "America/Los_Angeles"
65 |
66 | result = DateTime.shift_zone(utc_date_time, time_zone, Tz.TimeZoneDatabase)
67 |
68 | assert {:ok, pacific_datetime} = result
69 |
70 | assert DateTime.to_iso8601(pacific_datetime) == "2018-07-16T03:00:00-07:00"
71 | assert pacific_datetime.time_zone == "America/Los_Angeles"
72 | assert pacific_datetime.zone_abbr == "PDT"
73 | end
74 |
75 | test "time zone not found" do
76 | naive_date_time = ~N[2000-01-01 00:00:00]
77 | time_zone = "bad time zone"
78 |
79 | result = DateTime.from_naive(naive_date_time, time_zone, Tz.TimeZoneDatabase)
80 |
81 | assert {:error, :time_zone_not_found} = result
82 | end
83 |
84 | test "far future date" do
85 | naive_date_time = ~N[2043-12-18 12:30:00]
86 | time_zone = "Europe/Brussels"
87 |
88 | result = DateTime.from_naive(naive_date_time, time_zone, Tz.TimeZoneDatabase)
89 |
90 | assert {:ok, _datetime} = result
91 | end
92 |
93 | test "version" do
94 | assert Regex.match?(~r/202[2-9][a-z]/, Tz.iana_version())
95 | end
96 |
97 | test "time_zone_period_from_utc_iso_days with 0 and negative year" do
98 | utc_period = %{std_offset: 0, utc_offset: 0, zone_abbr: "UTC"}
99 | belgian_period = %{std_offset: 0, utc_offset: 1050, zone_abbr: "LMT"}
100 |
101 | iso_days = Calendar.ISO.naive_datetime_to_iso_days(0, 1, 1, 0, 0, 0, {0, 6})
102 |
103 | assert {:ok, utc_period} ==
104 | Tz.TimeZoneDatabase.time_zone_period_from_utc_iso_days(iso_days, "Etc/UTC")
105 |
106 | assert {:ok, belgian_period} ==
107 | Tz.TimeZoneDatabase.time_zone_period_from_utc_iso_days(iso_days, "Europe/Brussels")
108 |
109 | iso_days = Calendar.ISO.naive_datetime_to_iso_days(-1, 1, 1, 0, 0, 0, {0, 6})
110 |
111 | assert {:ok, utc_period} ==
112 | Tz.TimeZoneDatabase.time_zone_period_from_utc_iso_days(iso_days, "Etc/UTC")
113 |
114 | assert {:ok, belgian_period} ==
115 | Tz.TimeZoneDatabase.time_zone_period_from_utc_iso_days(iso_days, "Europe/Brussels")
116 | end
117 |
118 | test "time_zone_periods_from_wall_datetime with 0 and negative year" do
119 | utc_period = %{std_offset: 0, utc_offset: 0, zone_abbr: "UTC"}
120 | belgian_period = %{std_offset: 0, utc_offset: 1050, zone_abbr: "LMT"}
121 |
122 | naive_datetime = NaiveDateTime.new!(0, 1, 1, 0, 0, 0, {0, 6})
123 |
124 | assert {:ok, utc_period} ==
125 | Tz.TimeZoneDatabase.time_zone_periods_from_wall_datetime(naive_datetime, "Etc/UTC")
126 |
127 | assert {:ok, belgian_period} ==
128 | Tz.TimeZoneDatabase.time_zone_periods_from_wall_datetime(
129 | naive_datetime,
130 | "Europe/Brussels"
131 | )
132 |
133 | naive_datetime = NaiveDateTime.new!(-1, 1, 1, 0, 0, 0, {0, 6})
134 |
135 | assert {:ok, utc_period} ==
136 | Tz.TimeZoneDatabase.time_zone_periods_from_wall_datetime(naive_datetime, "Etc/UTC")
137 |
138 | assert {:ok, belgian_period} ==
139 | Tz.TimeZoneDatabase.time_zone_periods_from_wall_datetime(
140 | naive_datetime,
141 | "Europe/Brussels"
142 | )
143 | end
144 |
145 | test "convert non-iso datetime to iso" do
146 | non_iso_datetime = NaiveDateTime.convert!(~N[2000-01-01 13:30:15], HoloceneCalendar)
147 |
148 | assert Tz.TimeZoneDatabase.time_zone_periods_from_wall_datetime(non_iso_datetime, "Etc/UTC")
149 |
150 | assert Tz.TimeZoneDatabase.time_zone_periods_from_wall_datetime(
151 | non_iso_datetime,
152 | "Europe/Brussels"
153 | )
154 | end
155 |
156 | test "fix issue #24" do
157 | date_time_utc = ~U[2029-12-31 10:15:00Z]
158 | time_zone = "Pacific/Chatham"
159 |
160 | zoned_date_time = date_time_utc |> DateTime.shift_zone!(time_zone, Tz.TimeZoneDatabase)
161 | # #DateTime<2030-01-01 00:00:00+13:45 +1345 Pacific/Chatham>
162 |
163 | naive_datetime = DateTime.to_naive(zoned_date_time)
164 | # ~N[2030-01-01 00:00:00]
165 |
166 | assert zoned_date_time == DateTime.from_naive!(naive_datetime, time_zone, Tz.TimeZoneDatabase)
167 |
168 | naive_datetime = NaiveDateTime.from_iso8601!("2030-01-01T00:00:00")
169 | datetime = DateTime.from_naive!(naive_datetime, "Europe/Lisbon", Tz.TimeZoneDatabase)
170 |
171 | assert DateTime.to_iso8601(datetime) == "2030-01-01T00:00:00+00:00"
172 | end
173 |
174 | test "next_period/1" do
175 | {:ok, dt} =
176 | DateTime.new(~D[2030-09-01], ~T[10:00:00], "Europe/Copenhagen", Tz.TimeZoneDatabase)
177 |
178 | {from, _, _, _} = Tz.PeriodsProvider.next_period(dt)
179 |
180 | datetime_next_period =
181 | DateTime.from_gregorian_seconds(from)
182 | |> DateTime.shift_zone!(dt.time_zone, Tz.TimeZoneDatabase)
183 |
184 | {:ambiguous, first_dt, second_dt} =
185 | DateTime.new(~D[2030-10-27], ~T[02:00:00], "Europe/Copenhagen", Tz.TimeZoneDatabase)
186 |
187 | assert DateTime.compare(datetime_next_period, second_dt) == :eq
188 |
189 | {from, _, _, _} = Tz.PeriodsProvider.next_period(second_dt)
190 |
191 | datetime_next_period =
192 | DateTime.from_gregorian_seconds(from)
193 | |> DateTime.shift_zone!(dt.time_zone, Tz.TimeZoneDatabase)
194 |
195 | {:gap, _dt_just_before, dt_just_after} =
196 | DateTime.new(~D[2031-03-30], ~T[02:30:00], "Europe/Copenhagen", Tz.TimeZoneDatabase)
197 |
198 | assert DateTime.compare(datetime_next_period, dt_just_after) == :eq
199 |
200 | {from, _, _, _} = Tz.PeriodsProvider.next_period(first_dt)
201 |
202 | datetime_next_period =
203 | DateTime.from_gregorian_seconds(from)
204 | |> DateTime.shift_zone!(dt.time_zone, Tz.TimeZoneDatabase)
205 |
206 | {:ambiguous, _first_dt, second_dt} =
207 | DateTime.new(~D[2030-10-27], ~T[02:00:00], "Europe/Copenhagen", Tz.TimeZoneDatabase)
208 |
209 | assert DateTime.compare(datetime_next_period, second_dt) == :eq
210 |
211 | {:ok, dt} =
212 | DateTime.new(~D[2030-09-01], ~T[10:00:00], "Asia/Manila", Tz.TimeZoneDatabase)
213 |
214 | assert Tz.PeriodsProvider.next_period(dt) == nil
215 | end
216 |
217 | test "zone_abbr parameter" do
218 | ndt = ~N"2015-01-13 19:00:07"
219 | dt = DateTime.from_naive!(ndt, "Etc/UTC")
220 | dt = DateTime.shift_zone!(dt, "Etc/GMT+12", Tz.TimeZoneDatabase)
221 | assert dt.zone_abbr == "-12"
222 | end
223 | end
224 |
--------------------------------------------------------------------------------
/test/updater_test.exs:
--------------------------------------------------------------------------------
1 | defmodule UpdaterTest do
2 | use ExUnit.Case
3 |
4 | test "updater is only started once" do
5 | assert {:ok, _} = start_supervised(Tz.UpdatePeriodically)
6 | assert {:error, _} = start_supervised(Tz.UpdatePeriodically)
7 |
8 | assert {:ok, _} = start_supervised(Tz.WatchPeriodically)
9 | assert {:error, _} = start_supervised(Tz.WatchPeriodically)
10 |
11 | :timer.sleep(6_000)
12 | end
13 | end
14 |
--------------------------------------------------------------------------------