├── .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 | --------------------------------------------------------------------------------