├── .dialyzer_ignore_warnings ├── .formatter.exs ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── bench ├── format.exs └── profile.exs ├── config ├── config.exs ├── dev.exs ├── prod.exs ├── release.exs └── test.exs ├── lib └── cldr │ ├── backend │ ├── backend.ex │ ├── date_time.ex │ ├── format.ex │ ├── formatter.ex │ ├── interval.ex │ ├── interval │ │ ├── date.ex │ │ ├── date_time.ex │ │ └── time.ex │ └── relative.ex │ ├── date.ex │ ├── date_time.ex │ ├── datetime │ ├── exception.ex │ └── relative.ex │ ├── format │ ├── compiler.ex │ ├── date_time_format.ex │ ├── date_time_formatter.ex │ └── date_time_timezone.ex │ ├── interval.ex │ ├── interval │ ├── date.ex │ ├── date_time.ex │ └── time.ex │ ├── protocol │ └── cldr_chars.ex │ └── time.ex ├── logo.png ├── mix.exs ├── mix.lock ├── mix ├── for_dialyzer.ex └── my_app_backend.ex ├── src ├── date_time_format_lexer.xrl └── skeleton_tokenizer.xrl └── test ├── backend_doc_test.exs ├── cldr_chars_test.exs ├── cldr_dates_times_test.exs ├── date_time_relative_test.exs ├── doc_test.exs ├── duration_format_test.exs ├── exceptions_test.exs ├── interval_test.exs ├── partial_date_times_test.exs ├── test_helper.exs ├── variant_test.exs └── wrapper_test.exs /.dialyzer_ignore_warnings: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elixir-cldr/cldr_dates_times/a7df9b8103d4f9edd8cb731552396f2efc6ed112/.dialyzer_ignore_warnings -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | inputs: ["mix.exs", "{config,lib,test,mix}/**/*.{ex,exs}"], 3 | locals_without_parens: [docp: 1], 4 | line_length: 100 5 | ] 6 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Elixir CI 2 | 3 | # Define workflow that runs when changes are pushed to the 4 | # `main` branch or pushed to a PR branch that targets the `main` 5 | # branch. Change the branch name if your project uses a 6 | # different name for the main branch like "master" or "production". 7 | on: 8 | push: 9 | branches: [ "main" ] # adapt branch for project 10 | pull_request: 11 | branches: [ "main" ] # adapt branch for project 12 | 13 | # Sets the ENV `MIX_ENV` to `test` for running tests 14 | env: 15 | MIX_ENV: test 16 | 17 | permissions: 18 | contents: read 19 | 20 | jobs: 21 | test: 22 | # Set up a Postgres DB service. By default, Phoenix applications 23 | # use Postgres. This creates a database for running tests. 24 | # Additional services can be defined here if required. 25 | # services: 26 | # db: 27 | # image: postgres:12 28 | # ports: ['5432:5432'] 29 | # env: 30 | # POSTGRES_PASSWORD: postgres 31 | # options: >- 32 | # --health-cmd pg_isready 33 | # --health-interval 10s 34 | # --health-timeout 5s 35 | # --health-retries 5 36 | 37 | runs-on: ubuntu-latest 38 | name: Test on OTP ${{matrix.otp}} / Elixir ${{matrix.elixir}} 39 | strategy: 40 | # Specify the OTP and Elixir versions to use when building 41 | # and running the workflow steps. 42 | matrix: 43 | otp: ['27.0'] # Define the OTP version [required] 44 | elixir: ['1.17.2-otp-27'] # Define the elixi 45 | steps: 46 | # Step: Setup Elixir + Erlang image as the base. 47 | - name: Set up Elixir 48 | uses: erlef/setup-beam@v1 49 | with: 50 | otp-version: ${{matrix.otp}} 51 | elixir-version: ${{matrix.elixir}} 52 | 53 | # Step: Check out the code. 54 | - name: Checkout code 55 | uses: actions/checkout@v3 56 | 57 | # Step: Define how to cache deps. Restores existing cache if present. 58 | - name: Cache deps 59 | id: cache-deps 60 | uses: actions/cache@v3 61 | env: 62 | cache-name: cache-elixir-deps 63 | with: 64 | path: deps 65 | key: ${{ runner.os }}-mix-${{ env.cache-name }}-${{ hashFiles('**/mix.lock') }} 66 | restore-keys: | 67 | ${{ runner.os }}-mix-${{ env.cache-name }}- 68 | 69 | # Step: Define how to cache the `_build` directory. After the first run, 70 | # this speeds up tests runs a lot. This includes not re-compiling our 71 | # project's downloaded deps every run. 72 | - name: Cache compiled build 73 | id: cache-build 74 | uses: actions/cache@v3 75 | env: 76 | cache-name: cache-compiled-build 77 | with: 78 | path: _build 79 | key: ${{ runner.os }}-mix-${{ env.cache-name }}-${{ hashFiles('**/mix.lock') }} 80 | restore-keys: | 81 | ${{ runner.os }}-mix-${{ env.cache-name }}- 82 | ${{ runner.os }}-mix- 83 | 84 | # Step: Download project dependencies. If unchanged, uses 85 | # the cached version. 86 | - name: Install dependencies 87 | run: mix deps.get 88 | 89 | # Step: Compile the project treating any warnings as errors. 90 | # Customize this step if a different behavior is desired. 91 | - name: Compiles without warnings 92 | run: mix compile --warnings-as-errors 93 | 94 | # Step: Check that the checked in code has already been formatted. 95 | # This step fails if something was found unformatted. 96 | # Customize this step as desired. 97 | # - name: Check Formatting 98 | # run: mix format --check-formatted 99 | 100 | # Step: Execute the tests. 101 | - name: Run tests 102 | run: mix test 103 | 104 | # Step: Execute dialyzer. 105 | - name: Run dialyzer 106 | run: mix dialyzer 107 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /cover 3 | /deps 4 | /doc 5 | /references 6 | *.snapshot 7 | erl_crash.dump 8 | *.ez 9 | *.tar 10 | .DS_Store 11 | 12 | # The xml downloaded from Unicode 13 | /downloads 14 | 15 | # Ignore the generated json files 16 | /data 17 | 18 | # Generated *.erl 19 | src/*.erl 20 | 21 | .tools-versions 22 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | ## License 2 | 3 | Copyright 2017 Kip Cole 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in 6 | compliance with the License. You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software distributed under the License 11 | is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | implied. See the License for the specific language governing permissions and limitations under the 13 | License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Date and Time Localization and Formatting 2 | 3 | ![Build status](https://github.com/elixir-cldr/cldr_dates_times/actions/workflows/ci.yml/badge.svg) 4 | [![Hex.pm](https://img.shields.io/hexpm/v/ex_cldr_dates_times.svg)](https://hex.pm/packages/ex_cldr_dates_times) 5 | [![Hex.pm](https://img.shields.io/hexpm/dw/ex_cldr_dates_times.svg?)](https://hex.pm/packages/ex_cldr_dates_times) 6 | [![Hex.pm](https://img.shields.io/hexpm/dt/ex_cldr_dates_times.svg?)](https://hex.pm/packages/ex_cldr_dates_times) 7 | [![Hex.pm](https://img.shields.io/hexpm/l/ex_cldr_dates_times.svg)](https://hex.pm/packages/ex_cldr_dates_times) 8 | 9 | ## Installation 10 | 11 | **Note that `ex_cldr_dates_times` requires Elixir 1.12 or later.** 12 | 13 | Add `ex_cldr_dates_time` as a dependency to your `mix` project: 14 | 15 | ``` 16 | defp deps do 17 | [ 18 | {:ex_cldr_dates_times, "~> 2.0"} 19 | ] 20 | end 21 | ``` 22 | 23 | then retrieve `ex_cldr_dates_times` from [hex](https://hex.pm/packages/ex_cldr_dates_times): 24 | 25 | ``` 26 | mix deps.get 27 | mix deps.compile 28 | ``` 29 | 30 | ### Configuring a required backend module 31 | 32 | `ex_cldr_dates_times` uses the configuration set for the dependency `ex_cldr`. See the documentation for [ex_cldr](https://hexdocs.pm/ex_cldr/2.0.0/readme.html#configuration). 33 | 34 | A `backend` module is required that is used to host the functions that manage CLDR data. An example to get started is: 35 | 36 | 1. Create a backend module (see [ex_cldr](https://hexdocs.pm/ex_cldr/2.0.0/readme.html#configuration) for details of the available options). Note the requirement to configure the appropriate `Cldr` provider backends. 37 | 38 | ```elixir 39 | defmodule MyApp.Cldr do 40 | use Cldr, 41 | locales: ["en", "fr", "ja"], 42 | providers: [Cldr.Number, Cldr.Calendar, Cldr.DateTime] 43 | 44 | end 45 | ``` 46 | 47 | 2. [Optional] Update `config.exs` configuration to specify this backend as the system default. Not required, but often useful. 48 | 49 | ```elixir 50 | config :ex_cldr, 51 | default_locale: "en", 52 | default_backend: MyApp.Cldr 53 | ``` 54 | 55 | ## Introduction 56 | 57 | `ex_cldr_dates_times` is an addon library application for [ex_cldr](https://hex.pm/packages/ex_cldr) that provides localisation and formatting for dates, times and date_times. 58 | 59 | The primary API is `MyApp.Cldr.Date.to_string/2`, `MyApp.Cldr.Time.to_string/2`, `MyApp.Cldr.DateTime.to_string/2` and `MyApp.Cldr.DateTime.Relative.to_string/2`. In the following examples `MyApp` refers to a CLDR backend module that must be defined by the developer: 60 | 61 | ```elixir 62 | iex> MyApp.Cldr.Date.to_string ~D[2020-05-30] 63 | {:ok, "May 30, 2020"} 64 | 65 | iex> MyApp.Cldr.Time.to_string ~U[2020-05-30 03:52:56Z] 66 | {:ok, "3:52:56 AM"} 67 | 68 | iex> MyApp.Cldr.DateTime.to_string ~U[2020-05-30 03:52:56Z] 69 | {:ok, "May 30, 2020, 3:52:56 AM"} 70 | 71 | # Note that if options are provided, a backend 72 | # module is also required 73 | iex> MyApp.Cldr.DateTime.Relative.to_string 1, unit: :day, format: :narrow 74 | {:ok, "tomorrow"} 75 | ``` 76 | 77 | For help in `iex`: 78 | 79 | ```elixir 80 | iex> h MyApp.Cldr.Date.to_string 81 | iex> h MyApp.Cldr.Time.to_string 82 | iex> h MyApp.Cldr.DateTime.to_string 83 | iex> h MyApp.Cldr.DateTime.Relative.to_string 84 | ``` 85 | 86 | ## Date, Time and DateTime Localization Formats 87 | 88 | Dates, Times and DateTimes can be formatted using standard formats, format strings or format IDs. 89 | 90 | * Standard formats provide cross-locale standardisation and therefore should be preferred where possible. The format types, implemented for `MyApp.Cldr.Date.to_string/2`, `MyApp.Cldr.Time.to_string/2`,`MyApp.Cldr.DateTime.to_string/2` are `:short`, `:medium`, `:long` and `:full`. The default is `:medium`. For example, assuming a configured backend called `MyApp.Cldr`: 91 | 92 | ```elixir 93 | iex> MyApp.Cldr.DateTime.to_string ~U[2020-05-30 03:52:56Z], format: :short 94 | {:ok, "5/30/20, 3:52 AM"} 95 | 96 | iex> MyApp.Cldr.DateTime.to_string ~U[2020-05-30 03:52:56Z], format: :long 97 | {:ok, "May 30, 2020 at 3:52:56 AM UTC"} 98 | 99 | iex> MyApp.Cldr.DateTime.to_string ~U[2020-05-30 03:52:56Z], format: :medium 100 | {:ok, "May 30, 2020, 3:52:56 AM"} 101 | 102 | iex> MyApp.Cldr.DateTime.to_string ~U[2020-05-30 03:52:56Z], format: :long, locale: "fr" 103 | {:ok, "30 mai 2020 à 03:52:56 UTC"} 104 | ``` 105 | 106 | * Format strings use one or more formatting symbols to define what date and time elements should be placed in the format. A simple example to format the time into hours and minutes: 107 | 108 | ```elixir 109 | iex> MyApp.Cldr.DateTime.to_string ~U[2020-05-30 03:52:56Z], format: "hh:mm" 110 | {:ok, "03:52"} 111 | ``` 112 | 113 | * Format IDs are an atom that is a key into a map of formats defined by CLDR. These format IDs are returned by `MyApp.Cldr.DateTime.Format.date_time_available_formats/2` (assuming your backend is `MyApp.Cldr`). The set of common format names across all locales configured in `ex_cldr` can be returned by `MyApp.Cldr.DateTime.Format.common_date_time_format_names/0`. 114 | 115 | ```elixir 116 | iex> MyApp.Cldr.DateTime.Format.date_time_available_formats() 117 | %{mmmm_w_count_one: "'week' W 'of' MMMM", gy_mmm: "MMM y G", md: "M/d", 118 | mmm_md: "MMMM d", e_hms: "E HH:mm:ss", ed: "d E", y_mmm: "MMM y", 119 | e_hm: "E HH:mm", mmm_ed: "E, MMM d", y_mmm_ed: "E, MMM d, y", 120 | gy_mm_md: "MMM d, y G", mmm: "LLL", y_md: "M/d/y", gy: "y G", 121 | hms: "h:mm:ss a", hm: "h:mm a", y_mmmm: "MMMM y", m: "L", 122 | gy_mmm_ed: "E, MMM d, y G", y_qqq: "QQQ y", e: "ccc", y_qqqq: "QQQQ y", 123 | hmsv: "h:mm:ss a v", mmmm_w_count_other: "'week' W 'of' MMMM", 124 | ehm: "E h:mm a", y_m_ed: "E, M/d/y", h: "h a", hmv: "h:mm a v", 125 | yw_count_other: "'week' w 'of' y", mm_md: "MMM d", y_m: "M/y", m_ed: "E, M/d", 126 | ms: "mm:ss", d: "d", y_mm_md: "MMM d, y", yw_count_one: "'week' w 'of' y", 127 | y: "y", ehms: "E h:mm:ss a"} 128 | 129 | # These format types can be invoked for any locale - meaning 130 | # these format names are defined for all configured locales. 131 | iex> MyApp.Cldr.DateTime.Format.common_date_time_format_names() 132 | [:gy_mmm, :md, :mmm_md, :e_hms, :ed, :y_mmm, :e_hm, :mmm_ed, :y_mmm_ed, 133 | :gy_mm_md, :mmm, :y_md, :gy, :hms, :hm, :y_mmmm, :m, :gy_mmm_ed, :y_qqq, :e, 134 | :y_qqqq, :hmsv, :mmmm_w_count_other, :ehm, :y_m_ed, :h, :hmv, :yw_count_other, 135 | :mm_md, :y_m, :m_ed, :ms, :d, :y_mm_md, :y, :ehms] 136 | 137 | iex> Cldr.DateTime.to_string ~U[2020-05-30 03:52:56Z], MyApp.Cldr, format: :gy_mmm_ed 138 | {:ok, "Sat, May 30, 2020 AD"} 139 | 140 | iex(2)> Cldr.Time.to_string ~U[2020-05-30 03:52:56Z], MyApp.Cldr, format: :hm 141 | {:ok, "3:52 AM"} 142 | 143 | iex(3)> Cldr.Date.to_string ~U[2020-05-30 03:52:56Z], MyApp.Cldr, format: :yMd 144 | {:ok, "5/30/2020"} 145 | ``` 146 | 147 | ## Format strings 148 | 149 | The [CLDR standard](http://unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table) 150 | defines a wide range of format symbols. Most - but not all - of these symbols are supported in 151 | `Cldr`. The supported symbols are described below. Note the [known restrictions and limitations](#known-restrictions-and-limitations). 152 | 153 | | Element | Symbol | Example | Cldr Format | 154 | | :-------------------- | :-------- | :-------------- | :--------------------------------- | 155 | | Era | G, GG, GGG | "AD" | Abbreviated | 156 | | | GGGG | "Anno Domini" | Wide | 157 | | | GGGGG | "A" | Narrow | 158 | | Year | y | 7 | Minimum necessary digits | 159 | | | yy | "17" | Least significant 2 digits | 160 | | | yyy | "017", "2017" | Padded to at least 3 digits | 161 | | | yyyy | "2017" | Padded to at least 4 digits | 162 | | | yyyyy | "02017" | Padded to at least 5 digits | 163 | | ISOWeek Year | Y | 7 | Minimum necessary digits | 164 | | | YY | "17" | Least significant 2 digits | 165 | | | YYY | "017", "2017" | Padded to at least 3 digits | 166 | | | YYYY | "2017" | Padded to at least 4 digits | 167 | | | YYYYY | "02017" | Padded to at least 5 digits | 168 | | Related Gregorian Year | r, rr, rr+ | 2017 | Minimum necessary digits | 169 | | Cyclic Year | U, UU, UUU | "甲子" | Abbreviated | 170 | | | UUUU | "甲子" (for now) | Wide | 171 | | | UUUUU | "甲子" (for now) | Narrow | 172 | | Extended Year | u+ | 4601 | Minimum necessary digits | 173 | | Quarter | Q | 2 | Single digit | 174 | | | QQ | "02" | Two digits | 175 | | | QQQ | "Q2" | Abbreviated | 176 | | | QQQQ | "2nd quarter" | Wide | 177 | | | QQQQQ | "2" | Narrow | 178 | | Standalone Quarter | q | 2 | Single digit | 179 | | | qq | "02" | Two digits | 180 | | | qqq | "Q2" | Abbreviated | 181 | | | qqqq | "2nd quarter" | Wide | 182 | | | qqqqq | "2" | Narrow | 183 | | Month | M | 9 | Single digit | 184 | | | MM | "09" | Two digits | 185 | | | MMM | "Sep" | Abbreviated | 186 | | | MMMM | "September" | Wide | 187 | | | MMMMM | "S" | Narrow | 188 | | Standalone Month | L | 9 | Single digit | 189 | | | LL | "09" | Two digits | 190 | | | LLL | "Sep" | Abbreviated | 191 | | | LLLL | "September" | Wide | 192 | | | LLLLL | "S" | Narrow | 193 | | Week of Year | w | 2, 22 | Single digit | 194 | | | ww | 02, 22 | Two digits, zero padded | 195 | | Week of Month | W | 2 | Single digit. NOT IMPLEMENTED YET | 196 | | Day of Year | D | 3, 33, 333 | Minimum necessary digits | 197 | | | DD | 03, 33, 333 | Minimum of 2 digits, zero padded | 198 | | | DDD | 003, 033, 333 | Minimum of 3 digits, zero padded | 199 | | Day of Month | d | 2, 22 | Minimum necessary digits | 200 | | | dd | 02, 22 | Two digits, zero padded | 201 | | Day of Week | E, EE, EEE | "Tue" | Abbreviated | 202 | | | EEEE | "Tuesday" | Wide | 203 | | | EEEEE | "T" | Narrow | 204 | | | EEEEEE | "Tu" | Short | 205 | | | e | 2 | Single digit | 206 | | | ee | "02" | Two digits | 207 | | | eee | "Tue" | Abbreviated | 208 | | | eeee | "Tuesday" | Wide | 209 | | | eeeee | "T" | Narrow | 210 | | | eeeeee | "Tu" | Short | 211 | | Standalone Day of Week | c, cc | 2 | Single digit | 212 | | | ccc | "Tue" | Abbreviated | 213 | | | cccc | "Tuesday" | Wide | 214 | | | ccccc | "T" | Narrow | 215 | | | cccccc | "Tu" | Short | 216 | | AM or PM | a, aa, aaa | "am." | Abbreviated | 217 | | | aaaa | "am." | Wide | 218 | | | aaaaa | "am" | Narrow | 219 | | Noon, Mid, AM, PM | b, bb, bbb | "mid." | Abbreviated | 220 | | | bbbb | "midnight" | Wide | 221 | | | bbbbb | "md" | Narrow | 222 | | Flexible time period | B, BB, BBB | "at night" | Abbreviated | 223 | | | BBBB | "at night" | Wide | 224 | | | BBBBB | "at night" | Narrow | 225 | | Hour | h, K, H, k | | See the table below | 226 | | Minute | m | 3, 10 | Minimum digits of minutes | 227 | | | mm | "03", "12" | Two digits, zero padded | 228 | | Second | s | 3, 48 | Minimum digits of seconds | 229 | | | ss | "03", "48" | Two digits, zero padded | 230 | | Fractional Seconds | S | 3, 48 | Minimum digits of fractional seconds | 231 | | | SS | "03", "48" | Two digits, zero padded | 232 | | Milliseconds | A+ | 4000, 63241 | Minimum digits of milliseconds since midnight | 233 | | Generic non-location TZ | v | "Etc/UTC" | `:time_zone` key, unlocalised | 234 | | | vvvv | "unk" | Generic timezone name. Currently returns only "unk" | 235 | | Specific non-location TZ | z..zzz | "UTC" | `:zone_abbr` key, unlocalised | 236 | | | zzzz | "GMT" | Delegates to `zone_gmt/4` | 237 | | Timezone ID | V | "unk" | `:zone_abbr` key, unlocalised | 238 | | | VV | "Etc/UTC | Delegates to `zone_gmt/4` | 239 | | | VVV | "Unknown City" | Exemplar city. Not supported. | 240 | | | VVVV | "GMT" | Delegates to `zone_gmt/4 | 241 | | ISO8601 Format | Z..ZZZ | "+0100" | ISO8601 Basic Format with hours and minutes | 242 | | | ZZZZ | "+01:00" | Delegates to `zone_gmt/4 | 243 | | | ZZZZZ | "+01:00:10" | ISO8601 Extended format with optional seconds | 244 | | ISO8601 plus Z | X | "+01" | ISO8601 Basic Format with hours and optional minutes or "Z" | 245 | | | XX | "+0100" | ISO8601 Basic Format with hours and minutes or "Z" | 246 | | | XXX | "+0100" | ISO8601 Basic Format with hours and minutes, optional seconds or "Z" | 247 | | | XXXX | "+010059" | ISO8601 Basic Format with hours and minutes, optional seconds or "Z" | 248 | | | XXXXX | "+01:00:10" | ISO8601 Extended Format with hours and minutes, optional seconds or "Z" | 249 | | ISO8601 minus Z | x | "+0100" | ISO8601 Basic Format with hours and optional minutes | 250 | | | xx | "-0800" | ISO8601 Basic Format with hours and minutes | 251 | | | xxx | "+01:00" | ISO8601 Extended Format with hours and minutes | 252 | | | xxxx | "+010059" | ISO8601 Basic Format with hours and minutes, optional seconds | 253 | | | xxxxx | "+01:00:10" | ISO8601 Extended Format with hours and minutes, optional seconds | 254 | | GMT Format | O | "+0100" | Short localised GMT format | 255 | | | OOOO | "+010059" | Long localised GMT format | 256 | 257 | ## Formatting symbols for hour of day 258 | 259 | The hour of day can be formatted differently depending whether 260 | a 12- or 24-hour day is being represented and depending on the 261 | way in which midnight and noon are represented. The following 262 | table illustrates the differences: 263 | 264 | | Symbol | Midn. | Morning | Noon | Afternoon | Midn. | 265 | | :----: | :---: | :-----: | :--: | :--------: | :---: | 266 | | h | 12 | 1...11 | 12 | 1...11 | 12 | 267 | | K | 0 | 1...11 | 0 | 1...11 | 0 | 268 | | H | 0 | 1...11 | 12 | 13...23 | 0 | 269 | | k | 24 | 1...11 | 12 | 13...23 | 24 | 270 | 271 | ## Relative Date, Time and DateTime Localization Formatting 272 | 273 | The primary API for formatting relative dates and datetimes is `MyApp.Cldr.DateTime.Relative.to_string/2`. Some examples: 274 | 275 | ```elixir 276 | iex> MyApp.Cldr.DateTime.Relative.to_string(-1) 277 | {:ok, "1 second ago"} 278 | 279 | iex> MyApp.Cldr.DateTime.Relative.to_string(1) 280 | {:ok, "in 1 second"} 281 | 282 | iex> MyApp.Cldr.DateTime.Relative.to_string(1, unit: :day) 283 | {:ok, "tomorrow"} 284 | 285 | iex> MyApp.Cldr.DateTime.Relative.to_string(1, unit: :day, locale: "fr") 286 | {:ok, "demain"} 287 | 288 | iex> MyApp.Cldr.DateTime.Relative.to_string(1, unit: :day, format: :narrow) 289 | {:ok, "tomorrow"} 290 | 291 | iex> MyApp.Cldr.DateTime.Relative.to_string(1234, unit: :year) 292 | {:ok, "in 1,234 years"} 293 | 294 | iex> MyApp.Cldr.DateTime.Relative.to_string(1234, unit: :year, locale: "fr") 295 | {:ok, "dans 1 234 ans"} 296 | 297 | iex> MyApp.Cldr.DateTime.Relative.to_string(31) 298 | {:ok, "in 31 seconds"} 299 | 300 | iex> MyApp.Cldr.DateTime.Relative.to_string(~D[2017-04-29], relative_to: ~D[2017-04-26]) 301 | {:ok, "in 3 days"} 302 | 303 | iex> MyApp.Cldr.DateTime.Relative.to_string(310, format: :short, locale: "fr") 304 | {:ok, "dans 5 min"} 305 | 306 | iex> MyApp.Cldr.DateTime.Relative.to_string(310, format: :narrow, locale: "fr") 307 | {:ok, "+5 min"} 308 | 309 | iex> MyApp.Cldr.DateTime.Relative.to_string 2, unit: :wed, format: :short 310 | {:ok, "in 2 Wed."} 311 | 312 | iex> MyApp.Cldr.DateTime.Relative.to_string 1, unit: :wed, format: :short 313 | {:ok, "next Wed."} 314 | 315 | iex> MyApp.Cldr.DateTime.Relative.to_string -1, unit: :wed, format: :short 316 | {:ok, "last Wed."} 317 | 318 | iex> MyApp.Cldr.DateTime.Relative.to_string -1, unit: :wed 319 | {:ok, "last Wednesday"} 320 | 321 | iex> MyApp.Cldr.DateTime.Relative.to_string -1, unit: :quarter 322 | {:ok, "last quarter"} 323 | 324 | iex> MyApp.Cldr.DateTime.Relative.to_string -1, unit: :mon, locale: "fr" 325 | {:ok, "lundi dernier"} 326 | 327 | iex> MyApp.Cldr.DateTime.Relative.to_string(~D[2017-04-29], unit: :ziggeraut) 328 | {:error, {Cldr.UnknownTimeUnit, 329 | "Unknown time unit :ziggeraut. Valid time units are [:day, :hour, :minute, :month, :second, :week, :year, :mon, :tue, :wed, :thu, :fri, :sat, :sun, :quarter]"}} 330 | ``` 331 | 332 | ## Interval Formatting 333 | 334 | Interval formats allow for software to format intervals like "Jan 10-12, 2008" as a shorter and more natural format than "Jan 10, 2008 - Jan 12, 2008". They are designed to take a start and end date, time or datetime plus a formatting pattern and use that information to produce a localized format. 335 | 336 | An interval is expressed as either a `from` and `to` date, time or datetime. Or it can also be a `Date.Range` or `CalendarInterval` from the [calendar_interval](https://hex.pm/packages/calendar_interval) library. 337 | 338 | `Cldr.Interval.to_string/3` function to format an interval based upon the type of the arguments: date, datetime or time. The modules `Cldr.Date.Interval`, `Cldr.Time.Interval` and `Cldr.DateTime.Interval` also provide a `to_string/3` function for when the desired output format is more specific. 339 | 340 | Some examples: 341 | 342 | ```elixir 343 | iex> Cldr.Interval.to_string ~D[2020-01-01], ~D[2020-12-31], MyApp.Cldr 344 | {:ok, "Jan 1 – Dec 31, 2020"} 345 | 346 | iex> Cldr.Interval.to_string ~D[2020-01-01], ~D[2020-01-12], MyApp.Cldr 347 | {:ok, "Jan 1 – 12, 2020"} 348 | 349 | iex> Cldr.Interval.to_string ~D[2020-01-01], ~D[2020-01-12], MyApp.Cldr, 350 | ...> format: :long 351 | {:ok, "Wed, Jan 1 – Sun, Jan 12, 2020"} 352 | 353 | iex> Cldr.Interval.to_string ~D[2020-01-01], ~D[2020-12-01], MyApp.Cldr, 354 | ...> format: :long, style: :year_and_month 355 | {:ok, "January – December 2020"} 356 | 357 | iex> use CalendarInterval 358 | iex> Cldr.Interval.to_string ~I"2020-01-01/12", MyApp.Cldr, 359 | ...> format: :long 360 | {:ok, "Wed, Jan 1 – Sun, Jan 12, 2020"} 361 | 362 | iex> Cldr.Interval.to_string ~U[2020-01-01 00:00:00.0Z], ~U[2020-12-01 10:05:00.0Z], MyApp.Cldr, 363 | ...> format: :long 364 | {:ok, "January 1, 2020 at 12:00:00 AM UTC – December 1, 2020 at 10:05:00 AM UTC"} 365 | 366 | iex> Cldr.Interval.to_string ~U[2020-01-01 00:00:00.0Z], ~U[2020-01-01 10:05:00.0Z], MyApp.Cldr, 367 | ...> format: :long 368 | {:ok, "January 1, 2020 at 12:00:00 AM UTC – 10:05:00 AM UTC"} 369 | ``` 370 | 371 | ## Known restrictions and limitations 372 | 373 | Although largely complete (with respect to the CLDR data), there are some known limitations as of release 2.0. 374 | 375 | * *Timezones* Although the timezone format codes are supported (formatting symbols `v`, `V`, `x`, `X`, `z`, `Z`, `O`) not all localisations are performed. Only that data available within a `t:DateTime.t/0` struct is used to format timezone data. 376 | -------------------------------------------------------------------------------- /bench/format.exs: -------------------------------------------------------------------------------- 1 | date = DateTime.utc_now 2 | 3 | Benchee.run( 4 | %{ 5 | "Cldr.DateTime.to_string" => fn -> Cldr.DateTime.to_string date end, 6 | }, 7 | time: 10, 8 | memory_time: 2 9 | ) -------------------------------------------------------------------------------- /bench/profile.exs: -------------------------------------------------------------------------------- 1 | defmodule ProfileRunner do 2 | import ExProf.Macro 3 | 4 | @doc "analyze with profile macro" 5 | def do_analyze do 6 | today = DateTime.utc_now() 7 | 8 | profile do 9 | Cldr.DateTime.to_string today 10 | end 11 | end 12 | 13 | @doc "get analysis records and sum them up" 14 | def run do 15 | {records, _block_result} = do_analyze() 16 | 17 | records 18 | |> Enum.filter(&String.contains?(&1.function, "Cldr.DateTime")) 19 | |> ExProf.Analyzer.print 20 | end 21 | 22 | end 23 | 24 | ProfileRunner.run 25 | 26 | # 27 | # Total: 215 100.00% 328 [ 1.53] 28 | # %Prof{ 29 | # calls: 1, 30 | # function: "'Elixir.Cldr.Number.Formatter.Decimal':add_first_group/3", 31 | # percent: 0.0, 32 | # time: 0, 33 | # us_per_call: 0.0 34 | # } -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | import Config 4 | 5 | import_config "#{Mix.env()}.exs" 6 | -------------------------------------------------------------------------------- /config/dev.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :ex_cldr, 4 | default_locale: "en", 5 | default_backend: MyApp.Cldr, 6 | json_library: JSON 7 | 8 | config :elixir, :time_zone_database, Tz.TimeZoneDatabase 9 | -------------------------------------------------------------------------------- /config/prod.exs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elixir-cldr/cldr_dates_times/a7df9b8103d4f9edd8cb731552396f2efc6ed112/config/prod.exs -------------------------------------------------------------------------------- /config/release.exs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elixir-cldr/cldr_dates_times/a7df9b8103d4f9edd8cb731552396f2efc6ed112/config/release.exs -------------------------------------------------------------------------------- /config/test.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :ex_cldr, 4 | default_locale: "en", 5 | default_backend: MyApp.Cldr 6 | 7 | config :ex_unit, 8 | module_load_timeout: 220_000, 9 | case_load_timeout: 220_000, 10 | timeout: 220_000 11 | -------------------------------------------------------------------------------- /lib/cldr/backend/backend.ex: -------------------------------------------------------------------------------- 1 | defmodule Cldr.DateTime.Backend do 2 | @moduledoc false 3 | 4 | def define_date_time_modules(config) do 5 | quote location: :keep do 6 | unquote(Cldr.DateAndTime.Backend.define_backend_modules(config)) 7 | unquote(Cldr.DateTime.Format.Backend.define_date_time_format_module(config)) 8 | unquote(Cldr.DateTime.Formatter.Backend.define_date_time_formatter_module(config)) 9 | unquote(Cldr.DateTime.Relative.Backend.define_date_time_relative_module(config)) 10 | unquote(Cldr.Interval.Backend.define_interval_module(config)) 11 | unquote(Cldr.DateTime.Interval.Backend.define_date_time_interval_module(config)) 12 | unquote(Cldr.Date.Interval.Backend.define_date_interval_module(config)) 13 | unquote(Cldr.Time.Interval.Backend.define_time_interval_module(config)) 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/cldr/backend/formatter.ex: -------------------------------------------------------------------------------- 1 | defmodule Cldr.DateTime.Formatter.Backend do 2 | @moduledoc false 3 | 4 | def define_date_time_formatter_module(config) do 5 | backend = config.backend 6 | config = Macro.escape(config) 7 | module = inspect(__MODULE__) 8 | 9 | quote bind_quoted: [config: config, backend: backend, module: module] do 10 | defmodule DateTime.Formatter do 11 | @moduledoc false 12 | if Cldr.Config.include_module_docs?(config.generate_docs) do 13 | @moduledoc """ 14 | Implements the compilation and execution of 15 | date, time and datetime formats. 16 | 17 | """ 18 | end 19 | 20 | alias Cldr.DateTime.Format.Compiler 21 | alias Cldr.DateTime.Formatter 22 | alias Cldr.Number 23 | 24 | @doc """ 25 | Returns the formatted and localised date, time or datetime 26 | for a given `Date`, `Time`, `DateTime` or struct with the 27 | appropriate fields. 28 | 29 | ## Arguments 30 | 31 | * `date` is a `Date`, `Time`, `DateTime` or other struct that 32 | contains the required date and time fields. 33 | 34 | * `format` is a valid format string, for example `yy/MM/dd hh:MM` 35 | 36 | * `locale` is any valid locale name returned by `Cldr.known_locale_names/0` 37 | or a `t:Cldr.LanguageTag.t/0` struct. The default is `Cldr.get_locale/0` 38 | 39 | * `options` is a keyword list of options. 40 | 41 | ## Options 42 | 43 | * `:number_system`. The resulting formatted and localised date/time 44 | string will be transliterated into this number system. Number system 45 | is anything returned from `#{inspect(backend)}.Number.System.number_systems_for/1` 46 | 47 | *NOTE* This function is called by `Cldr.Date.to_string/2`, `Cldr.Time.to_string/2` 48 | and `Cldr.DateTime.to_string/2` which is the preferred API. 49 | 50 | ## Examples 51 | 52 | iex> #{inspect(__MODULE__)}.format ~U[2017-09-03 10:23:00.0Z], "yy/MM/dd hh:MM", "en" 53 | {:ok, "17/09/03 10:09"} 54 | 55 | """ 56 | @spec format( 57 | Cldr.Calendar.any_date_time(), 58 | String.t(), 59 | Cldr.Locale.locale_reference(), 60 | Keyword.t() 61 | ) :: {:ok, String.t()} | {:error, {module(), String.t()}} 62 | 63 | def format(date, format, locale \\ Cldr.get_locale(), options \\ []) 64 | 65 | # Insert generated functions for each locale and format here which 66 | # means that the lexing is done at compile time not runtime 67 | # which improves performance quite a bit. 68 | for format <- Cldr.DateTime.Format.format_list(config) do 69 | case Compiler.compile(format, backend, Cldr.DateTime.Formatter.Backend) do 70 | {:ok, transforms} -> 71 | def format(date, unquote(Macro.escape(format)) = f, locale, options) do 72 | format_transforms(date, f, unquote(transforms), locale, options) 73 | end 74 | 75 | {:error, message} -> 76 | raise Cldr.FormatCompileError, 77 | "#{message} compiling date format: #{inspect(format)}" 78 | end 79 | end 80 | 81 | # This is the format function that is executed if the supplied format 82 | # has not otherwise been precompiled in the code above. Since this function 83 | # has to tokenize, compile and then interpret the format string 84 | # there is a performance penalty. 85 | 86 | def format(date, format, locale, options) do 87 | case Compiler.tokenize(format) do 88 | {:ok, tokens, _} -> 89 | transforms = apply_transforms(tokens, date, locale, options) 90 | format_transforms(date, format, transforms, locale, options) 91 | 92 | {:error, {_, :date_time_format_lexer, {_, error}}, _} -> 93 | {:error, 94 | {Cldr.DateTime.Compiler.ParseError, 95 | "Could not tokenize #{inspect(format)}. Error detected at #{inspect(error)}"}} 96 | end 97 | end 98 | 99 | @doc false 100 | def format_transforms(date, format, transforms, locale, options) do 101 | number_system = 102 | number_system(format, options) 103 | 104 | formatted = 105 | Enum.reduce_while(transforms, "", fn 106 | {:error, reason}, acc -> {:halt, {:error, reason}} 107 | string, acc when is_binary(string) -> {:cont, acc <> string} 108 | number, acc when is_number(number) -> {:cont, acc <> to_string(number)} 109 | list, acc when is_list(list) -> {:cont, acc <> Enum.join(list)} 110 | end) 111 | 112 | case formatted do 113 | {:error, reason} -> 114 | {:error, reason} 115 | 116 | string -> 117 | transliterated = transliterate(string, locale, number_system) 118 | {:ok, transliterated} 119 | end 120 | end 121 | 122 | # Return the number system that is applied to the whole 123 | # formatted string at the end of formatting 124 | 125 | defp number_system(%{number_system: %{all: number_system}}, options) do 126 | number_system 127 | end 128 | 129 | defp number_system(_format, options) do 130 | options[:number_system] 131 | end 132 | 133 | # Return the map that drives number system transliteration 134 | # for individual formatting codes. 135 | 136 | defp format_number_systems(%{number_system: number_systems}) do 137 | number_systems 138 | end 139 | 140 | defp format_number_systems(_format) do 141 | %{} 142 | end 143 | 144 | # Execute the transformation pipeline which does the 145 | # actual formatting 146 | 147 | defp apply_transforms(tokens, date, locale, options) do 148 | Enum.map(tokens, fn {token, _line, count} -> 149 | apply(Cldr.DateTime.Formatter, token, [date, count, locale, unquote(backend), options]) 150 | end) 151 | end 152 | 153 | defp transliterate(formatted, _locale, nil) do 154 | formatted 155 | end 156 | 157 | defp transliterate(formatted, _locale, :latn) do 158 | formatted 159 | end 160 | 161 | transliterator = Module.concat(backend, :"Number.Transliterate") 162 | 163 | defp transliterate(formatted, locale, number_system) do 164 | with {:ok, number_system} <- 165 | Number.System.system_name_from(number_system, locale, unquote(backend)) do 166 | unquote(transliterator).transliterate_digits(formatted, :latn, number_system) 167 | end 168 | end 169 | 170 | defp format_errors(list) do 171 | errors = 172 | list 173 | |> Enum.filter(fn 174 | {:error, _reason} -> true 175 | _ -> false 176 | end) 177 | |> Enum.map(fn {:error, reason} -> reason end) 178 | 179 | if Enum.empty?(errors), do: nil, else: errors 180 | end 181 | 182 | # Compile the formats used for timezones GMT format 183 | def gmt_tz_format(locale, offset, options \\ []) 184 | 185 | for locale_name <- Cldr.Locale.Loader.known_locale_names(config) do 186 | {:ok, gmt_format} = Cldr.DateTime.Format.gmt_format(locale_name, backend) 187 | {:ok, gmt_zero_format} = Cldr.DateTime.Format.gmt_zero_format(locale_name, backend) 188 | {:ok, {pos_format, neg_format}} = Cldr.DateTime.Format.hour_format(locale_name, backend) 189 | 190 | {:ok, pos_transforms} = 191 | Compiler.compile(pos_format, backend, Cldr.DateTime.Formatter.Backend) 192 | 193 | {:ok, neg_transforms} = 194 | Compiler.compile(neg_format, backend, Cldr.DateTime.Formatter.Backend) 195 | 196 | def gmt_tz_format( 197 | %LanguageTag{cldr_locale_name: unquote(locale_name)}, 198 | %{hour: 0, minute: 0}, 199 | _options 200 | ) do 201 | unquote(gmt_zero_format) 202 | end 203 | 204 | def gmt_tz_format( 205 | %LanguageTag{cldr_locale_name: unquote(locale_name)} = locale, 206 | %{hour: hour, minute: _minute} = date, 207 | options 208 | ) 209 | when hour >= 0 do 210 | unquote(pos_transforms) 211 | |> Cldr.DateTime.Format.gmt_format_type(options[:format] || :long) 212 | |> Cldr.Substitution.substitute(unquote(gmt_format)) 213 | |> Enum.join() 214 | end 215 | 216 | def gmt_tz_format( 217 | %LanguageTag{cldr_locale_name: unquote(locale_name)} = locale, 218 | %{hour: _hour, minute: _minute} = date, 219 | options 220 | ) do 221 | unquote(neg_transforms) 222 | |> Cldr.DateTime.Format.gmt_format_type(options[:format] || :long) 223 | |> Cldr.Substitution.substitute(unquote(gmt_format)) 224 | |> Enum.join() 225 | end 226 | end 227 | end 228 | end 229 | end 230 | end 231 | -------------------------------------------------------------------------------- /lib/cldr/backend/interval.ex: -------------------------------------------------------------------------------- 1 | defmodule Cldr.Interval.Backend do 2 | @moduledoc false 3 | 4 | def define_interval_module(config) do 5 | backend = config.backend 6 | config = Macro.escape(config) 7 | 8 | quote location: :keep, bind_quoted: [config: config, backend: backend] do 9 | defmodule Interval do 10 | @moduledoc """ 11 | Interval formats allow for software to format intervals like "Jan 10-12, 2008" as a 12 | shorter and more natural format than "Jan 10, 2008 - Jan 12, 2008". They are designed 13 | to take a start and end date, time or datetime plus a formatting pattern 14 | and use that information to produce a localized format. 15 | 16 | The interval functions in this library will determine the calendar 17 | field with the greatest difference between the two datetimes before using the 18 | format pattern. 19 | 20 | For example, the greatest difference in "Jan 10-12, 2008" is the day field, while 21 | the greatest difference in "Jan 10 - Feb 12, 2008" is the month field. This is used to 22 | pick the exact pattern to be used. 23 | 24 | See `Cldr.Interval` for further detail. 25 | 26 | """ 27 | 28 | if Cldr.Code.ensure_compiled?(CalendarInterval) do 29 | @doc false 30 | def to_string(%CalendarInterval{} = interval) do 31 | Cldr.Interval.to_string(interval, unquote(backend), []) 32 | end 33 | end 34 | 35 | @doc false 36 | def to_string(%Elixir.Date.Range{} = interval) do 37 | Cldr.Interval.to_string(interval, unquote(backend), []) 38 | end 39 | 40 | @doc """ 41 | Returns a `Date.Range` or `CalendarInterval` as 42 | a localised string. 43 | 44 | ## Arguments 45 | 46 | * `range` is either a `Date.Range.t` returned from `Date.range/2` 47 | or a `CalendarInterval.t` 48 | 49 | * `options` is a keyword list of options. The default is 50 | `[format: :medium, style: :date | :time | nil]`. 51 | 52 | ## Options 53 | 54 | * `:format` is one of `:short`, `:medium` or `:long` or a 55 | specific format type or a string representing of an interval 56 | format. The default is `:medium`. 57 | 58 | * `:style` supports different formatting styles. The valid 59 | styles depends on whether formatting is for a date, time or datetime. 60 | Since the functions in this module will make a determination as 61 | to which formatter to be used based upon the data passed to them 62 | it is recommended the style option be omitted. If a style is important 63 | then call `to_string/3` directly on `Cldr.Date.Interval`, `Cldr.Time.Interval` 64 | or `Cldr.DateTime.Interval`. 65 | 66 | * For a date the alternatives are `:date`, `:month_and_day`, `:month` 67 | and `:year_and_month`. The default is `:date`. 68 | 69 | * For a time the alternatives are `:time`, `:zone` and 70 | `:flex`. The default is `:time` 71 | 72 | * For a datetime there are no style options, the default 73 | for each of the date and time part is used 74 | 75 | * `locale` is any valid locale name returned by `Cldr.known_locale_names/0` 76 | or a `t:Cldr.LanguageTag.t/0` struct. The default is `#{backend}.get_locale/0` 77 | 78 | * `number_system:` a number system into which the formatted date digits should 79 | be transliterated 80 | 81 | ## Returns 82 | 83 | * `{:ok, string}` or 84 | 85 | * `{:error, {exception, reason}}` 86 | 87 | ## Notes 88 | 89 | * `to_string/2` will decide which formatter to call based upon 90 | the arguments provided to it. 91 | 92 | * A `Date.Range.t` will call `Cldr.Date.Interval.to_string/3` 93 | 94 | * A `CalendarInterval` will call `Cldr.Date.Interval.to_string/3` 95 | if its `:precision` is `:year`, `:month` or `:day`. Othersie 96 | it will call `Cldr.Time.Interval.to_string/3` 97 | 98 | * If `from` and `to` both conform to the `Calendar.datetime()` 99 | type then `Cldr.DateTime.Interval.to_string/3` is called 100 | 101 | * Otherwise if `from` and `to` conform to the `Calendar.date()` 102 | type then `Cldr.Date.Interval.to_string/3` is called 103 | 104 | * Otherwise if `from` and `to` conform to the `Calendar.time()` 105 | type then `Cldr.Time.Interval.to_string/3` is called 106 | 107 | * `CalendarInterval` support requires adding the 108 | dependency [calendar_interval](https://hex.pm/packages/calendar_interval) 109 | to the `deps` configuration in `mix.exs`. 110 | 111 | * For more information on interval format string 112 | see `Cldr.Interval`. 113 | 114 | * The available predefined formats that can be applied are the 115 | keys of the map returned by `Cldr.DateTime.Format.interval_formats("en", :gregorian)` 116 | where `"en"` can be replaced by any configuration locale name and `:gregorian` 117 | is the underlying CLDR calendar type. 118 | 119 | * In the case where `from` and `to` are equal, a single 120 | date, time or datetime is formatted instead of an interval 121 | 122 | ## Examples 123 | 124 | iex> use CalendarInterval 125 | iex> #{inspect(__MODULE__)}.to_string ~I"2020-01-01/12", 126 | ...> format: :long 127 | {:ok, "Wed, Jan 1 – Sun, Jan 12, 2020"} 128 | 129 | iex> #{inspect(__MODULE__)}.to_string Date.range(~D[2020-01-01], ~D[2020-12-31]), 130 | ...> format: :long 131 | {:ok, "Wed, Jan 1 – Thu, Dec 31, 2020"} 132 | 133 | """ 134 | @spec to_string(Cldr.Interval.range(), Keyword.t()) :: 135 | {:ok, String.t()} | {:error, {module, String.t()}} 136 | 137 | if Cldr.Code.ensure_compiled?(CalendarInterval) do 138 | def to_string(%CalendarInterval{} = interval, options) do 139 | Cldr.Interval.to_string(interval, unquote(backend), options) 140 | end 141 | end 142 | 143 | def to_string(%Elixir.Date.Range{} = interval, options) do 144 | Cldr.Interval.to_string(interval, unquote(backend), options) 145 | end 146 | 147 | @doc """ 148 | Returns a string representing the formatted 149 | interval formed by two dates. 150 | 151 | ## Arguments 152 | 153 | * `from` is any map that conforms to the 154 | any one of the `Calendar` types. 155 | 156 | * `to` is any map that conforms to the 157 | any one of the `Calendar` types. `to` must 158 | occur on or after `from`. 159 | 160 | * `options` is a keyword list of options. The default is 161 | `[format: :medium, style: :date | :time | nil]`. 162 | 163 | ## Options 164 | 165 | * `:format` is one of `:short`, `:medium` or `:long` or a 166 | specific format type or a string representing of an interval 167 | format. The default is `:medium`. 168 | 169 | * `:style` supports different formatting styles. The valid 170 | styles depends on whether formatting is for a date, time or datetime. 171 | Since the functions in this module will make a determination as 172 | to which formatter to be used based upon the data passed to them 173 | it is recommended the style option be omitted. If styling is important 174 | then call `to_string/3` directly on `Cldr.Date.Interval`, `Cldr.Time.Interval` 175 | or `Cldr.DateTime.Interval`. 176 | 177 | * For a date the alternatives are `:date`, `:month_and_day`, `:month` 178 | and `:year_and_month`. The default is `:date`. 179 | 180 | * For a time the alternatives are `:time`, `:zone` and 181 | `:flex`. The default is `:time` 182 | 183 | * For a datetime there are no style options, the default 184 | for each of the date and time part is used 185 | 186 | * `locale` is any valid locale name returned by `Cldr.known_locale_names/0` 187 | or a `t:Cldr.LanguageTag.t/0` struct. The default is `#{backend}.get_locale/0` 188 | 189 | * `number_system:` a number system into which the formatted date digits should 190 | be transliterated. 191 | 192 | ## Returns 193 | 194 | * `{:ok, string}` or 195 | 196 | * `{:error, {exception, reason}}` 197 | 198 | ## Notes 199 | 200 | * `to_string/2` will decide which formatter to call based upon 201 | the arguments provided to it. 202 | 203 | * A `Date.Range.t` will call `Cldr.Date.Interval.to_string/3` 204 | 205 | * A `CalendarInterval` will call `Cldr.Date.Interval.to_string/3` 206 | if its `:precision` is `:year`, `:month` or `:day`. Othersie 207 | it will call `Cldr.Time.Interval.to_string/3` 208 | 209 | * If `from` and `to` both conform to the `Calendar.datetime()` 210 | type then `Cldr.DateTime.Interval.to_string/3` is called 211 | 212 | * Otherwise if `from` and `to` conform to the `Calendar.date()` 213 | type then `Cldr.Date.Interval.to_string/3` is called 214 | 215 | * Otherwise if `from` and `to` conform to the `Calendar.time()` 216 | type then `Cldr.Time.Interval.to_string/3` is called 217 | 218 | * `CalendarInterval` support requires adding the 219 | dependency [calendar_interval](https://hex.pm/packages/calendar_interval) 220 | to the `deps` configuration in `mix.exs`. 221 | 222 | * For more information on interval format string 223 | see `Cldr.Interval`. 224 | 225 | * The available predefined formats that can be applied are the 226 | keys of the map returned by `Cldr.DateTime.Format.interval_formats("en", :gregorian)` 227 | where `"en"` can be replaced by any configuration locale name and `:gregorian` 228 | is the underlying CLDR calendar type. 229 | 230 | * In the case where `from` and `to` are equal, a single 231 | date, time or datetime is formatted instead of an interval. 232 | 233 | ## Examples 234 | 235 | iex> #{inspect(__MODULE__)}.to_string(~D[2020-01-01], ~D[2020-12-31]) 236 | {:ok, "Jan 1 – Dec 31, 2020"} 237 | 238 | iex> #{inspect(__MODULE__)}.to_string(~D[2020-01-01], ~D[2020-01-12]) 239 | {:ok, "Jan 1 – 12, 2020"} 240 | 241 | iex> #{inspect(__MODULE__)}.to_string(~D[2020-01-01], ~D[2020-01-12], 242 | ...> format: :long) 243 | {:ok, "Wed, Jan 1 – Sun, Jan 12, 2020"} 244 | 245 | iex> #{inspect(__MODULE__)}.to_string(~D[2020-01-01], ~D[2020-12-01], 246 | ...> format: :long, style: :year_and_month) 247 | {:ok, "January – December 2020"} 248 | 249 | iex> use CalendarInterval 250 | iex> #{inspect(__MODULE__)}.to_string(~I"2020-01-01/12", 251 | ...> format: :long) 252 | {:ok, "Wed, Jan 1 – Sun, Jan 12, 2020"} 253 | 254 | iex> #{inspect(__MODULE__)}.to_string(~U[2020-01-01 00:00:00.0Z], ~U[2020-12-01 10:05:00.0Z], 255 | ...> format: :long) 256 | {:ok, "January 1, 2020, 12:00:00 AM UTC – December 1, 2020, 10:05:00 AM UTC"} 257 | 258 | iex> #{inspect(__MODULE__)}.to_string(~U[2020-01-01 00:00:00.0Z], ~U[2020-01-01 10:05:00.0Z], 259 | ...> format: :long) 260 | {:ok, "January 1, 2020, 12:00:00 AM UTC – 10:05:00 AM UTC"} 261 | 262 | """ 263 | @spec to_string(Cldr.Interval.datetime(), Cldr.Interval.datetime(), Keyword.t()) :: 264 | {:ok, String.t()} | {:error, {module, String.t()}} 265 | 266 | def to_string(from, to, options \\ []) do 267 | Cldr.Interval.to_string(from, to, unquote(backend), options) 268 | end 269 | 270 | if Cldr.Code.ensure_compiled?(CalendarInterval) do 271 | @doc false 272 | def to_string!(%CalendarInterval{} = interval) do 273 | Cldr.Interval.to_string!(interval, unquote(backend), []) 274 | end 275 | end 276 | 277 | @doc false 278 | def to_string!(%Elixir.Date.Range{} = interval) do 279 | Cldr.Interval.to_string!(interval, unquote(backend), []) 280 | end 281 | 282 | @doc """ 283 | Returns a `Date.Range` or `CalendarInterval` as 284 | a localised string or raises an exception. 285 | 286 | ## Arguments 287 | 288 | * `range` is either a `Date.Range.t` returned from `Date.range/2` 289 | or a `CalendarInterval.t` 290 | 291 | * `options` is a keyword list of options. The default is 292 | `[format: :medium, style: :date | :time | nil]`. 293 | 294 | ## Options 295 | 296 | * `:format` is one of `:short`, `:medium` or `:long` or a 297 | specific format type or a string representing of an interval 298 | format. The default is `:medium`. 299 | 300 | * `:style` supports different formatting styles. The valid 301 | styles depends on whether formatting is for a date, time or datetime. 302 | Since the functions in this module will make a determination as 303 | to which formatter to be used based upon the data passed to them 304 | it is recommended the style option be omitted. If a style is important 305 | then call `to_string/3` directly on `Cldr.Date.Interval`, `Cldr.Time.Interval` 306 | or `Cldr.DateTime.Interval`. 307 | 308 | * For a date the alternatives are `:date`, `:month_and_day`, `:month` 309 | and `:year_and_month`. The default is `:date`. 310 | 311 | * For a time the alternatives are `:time`, `:zone` and 312 | `:flex`. The default is `:time`. 313 | 314 | * For a datetime there are no style options, the default 315 | for each of the date and time part is used. 316 | 317 | * `locale` is any valid locale name returned by `Cldr.known_locale_names/0` 318 | or a `t:Cldr.LanguageTag.t/0` struct. The default is `#{backend}.get_locale/0`. 319 | 320 | * `number_system:` a number system into which the formatted date digits should 321 | be transliterated. 322 | 323 | ## Returns 324 | 325 | * `string` or 326 | 327 | * raises an exception 328 | 329 | ## Notes 330 | 331 | * `to_string/3` will decide which formatter to call based upon 332 | the arguments provided to it. 333 | 334 | * A `Date.Range.t` will call `Cldr.Date.Interval.to_string/3` 335 | 336 | * A `CalendarInterval` will call `Cldr.Date.Interval.to_string/3` 337 | if its `:precision` is `:year`, `:month` or `:day`. Otherwise 338 | it will call `Cldr.Time.Interval.to_string/3` 339 | 340 | * If `from` and `to` both conform to the `Calendar.datetime()` 341 | type then `Cldr.DateTime.Interval.to_string/3` is called 342 | 343 | * Otherwise if `from` and `to` conform to the `Calendar.date()` 344 | type then `Cldr.Date.Interval.to_string/3` is called 345 | 346 | * Otherwise if `from` and `to` conform to the `Calendar.time()` 347 | type then `Cldr.Time.Interval.to_string/3` is called 348 | 349 | * `CalendarInterval` support requires adding the 350 | dependency [calendar_interval](https://hex.pm/packages/calendar_interval) 351 | to the `deps` configuration in `mix.exs`. 352 | 353 | * For more information on interval format string 354 | see `Cldr.Interval`. 355 | 356 | * The available predefined formats that can be applied are the 357 | keys of the map returned by `Cldr.DateTime.Format.interval_formats("en", :gregorian)` 358 | where `"en"` can be replaced by any configuration locale name and `:gregorian` 359 | is the underlying CLDR calendar type. 360 | 361 | * In the case where `from` and `to` are equal, a single 362 | date, time or datetime is formatted instead of an interval 363 | 364 | ## Examples 365 | 366 | iex> use CalendarInterval 367 | iex> #{inspect(__MODULE__)}.to_string!(~I"2020-01-01/12", 368 | ...> format: :long) 369 | "Wed, Jan 1 – Sun, Jan 12, 2020" 370 | 371 | iex> #{inspect(__MODULE__)}.to_string!(Date.range(~D[2020-01-01], ~D[2020-12-31]), 372 | ...> format: :long) 373 | "Wed, Jan 1 – Thu, Dec 31, 2020" 374 | 375 | """ 376 | @spec to_string!(Cldr.Interval.range(), Keyword.t()) :: 377 | String.t() | no_return 378 | 379 | if Cldr.Code.ensure_compiled?(CalendarInterval) do 380 | def to_string!(%CalendarInterval{} = interval, options) do 381 | Cldr.Interval.to_string!(interval, unquote(backend), options) 382 | end 383 | end 384 | 385 | def to_string!(%Elixir.Date.Range{} = interval, options) do 386 | Cldr.Interval.to_string!(interval, unquote(backend), options) 387 | end 388 | 389 | @doc """ 390 | Returns a string representing the formatted 391 | interval formed by two date or raises an 392 | exception. 393 | 394 | ## Arguments 395 | 396 | * `from` is any map that conforms to the 397 | any one of the `Calendar` types. 398 | 399 | * `to` is any map that conforms to the 400 | any one of the `Calendar` types. `to` must 401 | occur on or after `from`. 402 | 403 | * `options` is a keyword list of options. The default is 404 | `[format: :medium, style: :date | :time | nil]`. 405 | 406 | ## Options 407 | 408 | * `:format` is one of `:short`, `:medium` or `:long` or a 409 | specific format type or a string representing of an interval 410 | format. The default is `:medium`. 411 | 412 | * `:style` supports different formatting styles. The valid 413 | styles depends on whether formatting is for a date, time or datetime. 414 | Since the functions in this module will make a determination as 415 | to which formatter to be used based upon the data passed to them 416 | it is recommended the style option be omitted. If styling is important 417 | then call `to_string/3` directly on `Cldr.Date.Interval`, `Cldr.Time.Interval` 418 | or `Cldr.DateTime.Interval`. 419 | 420 | * For a date the alternatives are `:date`, `:month_and_day`, `:month` 421 | and `:year_and_month`. The default is `:date`. 422 | 423 | * For a time the alternatives are `:time`, `:zone` and 424 | `:flex`. The default is `:time`. 425 | 426 | * For a datetime there are no style options, the default 427 | for each of the date and time part is used. 428 | 429 | * `locale` is any valid locale name returned by `Cldr.known_locale_names/0` 430 | or a `t:Cldr.LanguageTag.t/0` struct. The default is `#{backend}.get_locale/0`. 431 | 432 | * `number_system:` a number system into which the formatted date digits should 433 | be transliterated. 434 | 435 | ## Returns 436 | 437 | * `string` or 438 | 439 | * raises and exception 440 | 441 | ## Notes 442 | 443 | * `to_string/3` will decide which formatter to call based upon 444 | the arguments provided to it. 445 | 446 | * A `Date.Range.t` will call `Cldr.Date.Interval.to_string/3` 447 | 448 | * A `CalendarInterval` will call `Cldr.Date.Interval.to_string/3` 449 | if its `:precision` is `:year`, `:month` or `:day`. Othersie 450 | it will call `Cldr.Time.Interval.to_string/3` 451 | 452 | * If `from` and `to` both conform to the `Calendar.datetime()` 453 | type then `Cldr.DateTime.Interval.to_string/3` is called 454 | 455 | * Otherwise if `from` and `to` conform to the `Calendar.date()` 456 | type then `Cldr.Date.Interval.to_string/3` is called 457 | 458 | * Otherwise if `from` and `to` conform to the `Calendar.time()` 459 | type then `Cldr.Time.Interval.to_string/3` is called 460 | 461 | * `CalendarInterval` support requires adding the 462 | dependency [calendar_interval](https://hex.pm/packages/calendar_interval) 463 | to the `deps` configuration in `mix.exs`. 464 | 465 | * For more information on interval format string 466 | see `Cldr.Interval`. 467 | 468 | * The available predefined formats that can be applied are the 469 | keys of the map returned by `Cldr.DateTime.Format.interval_formats("en", :gregorian)` 470 | where `"en"` can be replaced by any configuration locale name and `:gregorian` 471 | is the underlying CLDR calendar type. 472 | 473 | * In the case where `from` and `to` are equal, a single 474 | date, time or datetime is formatted instead of an interval. 475 | 476 | ## Examples 477 | 478 | iex> #{inspect(__MODULE__)}.to_string!(~D[2020-01-01], ~D[2020-12-31]) 479 | "Jan 1 – Dec 31, 2020" 480 | 481 | iex> #{inspect(__MODULE__)}.to_string!(~D[2020-01-01], ~D[2020-01-12]) 482 | "Jan 1 – 12, 2020" 483 | 484 | iex> #{inspect(__MODULE__)}.to_string!(~D[2020-01-01], ~D[2020-01-12], 485 | ...> format: :long) 486 | "Wed, Jan 1 – Sun, Jan 12, 2020" 487 | 488 | iex> #{inspect(__MODULE__)}.to_string!(~D[2020-01-01], ~D[2020-12-01], 489 | ...> format: :long, style: :year_and_month) 490 | "January – December 2020" 491 | 492 | iex> use CalendarInterval 493 | iex> #{inspect(__MODULE__)}.to_string!(~I"2020-01-01/12", 494 | ...> format: :long) 495 | "Wed, Jan 1 – Sun, Jan 12, 2020" 496 | 497 | iex> #{inspect(__MODULE__)}.to_string!(~U[2020-01-01 00:00:00.0Z], ~U[2020-12-01 10:05:00.0Z], 498 | ...> format: :long) 499 | "January 1, 2020, 12:00:00 AM UTC – December 1, 2020, 10:05:00 AM UTC" 500 | 501 | iex> #{inspect(__MODULE__)}.to_string!(~U[2020-01-01 00:00:00.0Z], ~U[2020-01-01 10:05:00.0Z], 502 | ...> format: :long) 503 | "January 1, 2020, 12:00:00 AM UTC – 10:05:00 AM UTC" 504 | 505 | """ 506 | @spec to_string!(Cldr.Interval.datetime(), Cldr.Interval.datetime(), Keyword.t()) :: 507 | String.t() | no_return() 508 | 509 | def to_string!(from, to, options \\ []) do 510 | Cldr.Interval.to_string!(from, to, unquote(backend), options) 511 | end 512 | end 513 | end 514 | end 515 | end 516 | -------------------------------------------------------------------------------- /lib/cldr/backend/interval/date.ex: -------------------------------------------------------------------------------- 1 | defmodule Cldr.Date.Interval.Backend do 2 | @moduledoc false 3 | 4 | def define_date_interval_module(config) do 5 | backend = config.backend 6 | config = Macro.escape(config) 7 | 8 | quote location: :keep, bind_quoted: [config: config, backend: backend] do 9 | defmodule Date.Interval do 10 | @moduledoc """ 11 | Interval formats allow for software to format intervals like "Jan 10-12, 2008" as a 12 | shorter and more natural format than "Jan 10, 2008 - Jan 12, 2008". They are designed 13 | to take a start and end date, time or datetime plus a formatting pattern 14 | and use that information to produce a localized format. 15 | 16 | See `#{inspect(__MODULE__)}.to_string/3` and `#{inspect(backend)}.Interval.to_string/3` 17 | 18 | """ 19 | 20 | date = 21 | quote do 22 | %{ 23 | year: _, 24 | month: _, 25 | day: _, 26 | calendar: var!(calendar, unquote(__MODULE__)) 27 | } 28 | end 29 | 30 | if Cldr.Code.ensure_compiled?(CalendarInterval) do 31 | @doc false 32 | def to_string(%CalendarInterval{} = interval) do 33 | Cldr.Date.Interval.to_string(interval, unquote(backend), []) 34 | end 35 | end 36 | 37 | @doc false 38 | def to_string(%Elixir.Date.Range{} = interval) do 39 | Cldr.Date.Interval.to_string(interval, unquote(backend), []) 40 | end 41 | 42 | @doc """ 43 | Returns a `Date.Range` or `CalendarInterval` as 44 | a localised string. 45 | 46 | ## Arguments 47 | 48 | * `range` is either a `Date.Range.t` returned from `Date.range/2` 49 | or a `CalendarInterval.t` 50 | 51 | * `options` is a keyword list of options. The default is 52 | `[format: :medium, style: :date]`. 53 | 54 | ## Options 55 | 56 | * `:format` is one of `:short`, `:medium` or `:long` or a 57 | specific format type or a string representing of an interval 58 | format. The default is `:medium`. 59 | 60 | * `:style` supports different formatting styles. The 61 | alternatives are `:date`, `:month_and_day`, `:month` 62 | and `:year_and_month`. The default is `:date`. 63 | 64 | * `:locale` is any valid locale name returned by `Cldr.known_locale_names/0` 65 | or a `t:Cldr.LanguageTag.t/0` struct. The default is `#{backend}.get_locale/0` 66 | 67 | * `:number_system` a number system into which the formatted date digits should 68 | be transliterated 69 | 70 | ## Returns 71 | 72 | * `{:ok, string}` or 73 | 74 | * `{:error, {exception, reason}}` 75 | 76 | ## Notes 77 | 78 | * `CalendarInterval` support requires adding the 79 | dependency [calendar_interval](https://hex.pm/packages/calendar_interval) 80 | to the `deps` configuration in `mix.exs`. 81 | 82 | * For more information on interval format string 83 | see the `Cldr.Interval`. 84 | 85 | * The available predefined formats that can be applied are the 86 | keys of the map returned by `Cldr.DateTime.Format.interval_formats("en", :gregorian)` 87 | where `"en"` can be replaced by any configuration locale name and `:gregorian` 88 | is the underlying CLDR calendar type. 89 | 90 | * In the case where `from` and `to` are equal, a single 91 | date is formatted instead of an interval 92 | 93 | ## Examples 94 | 95 | iex> #{inspect(__MODULE__)}.to_string Date.range(~D[2020-01-01], ~D[2020-12-31]) 96 | {:ok, "Jan 1 – Dec 31, 2020"} 97 | 98 | iex> #{inspect(__MODULE__)}.to_string Date.range(~D[2020-01-01], ~D[2020-01-12]) 99 | {:ok, "Jan 1 – 12, 2020"} 100 | 101 | iex> #{inspect(__MODULE__)}.to_string Date.range(~D[2020-01-01], ~D[2020-01-12]), 102 | ...> format: :long 103 | {:ok, "Wed, Jan 1 – Sun, Jan 12, 2020"} 104 | 105 | iex> #{inspect(__MODULE__)}.to_string Date.range(~D[2020-01-01], ~D[2020-12-01]), 106 | ...> format: :long, style: :year_and_month 107 | {:ok, "January – December 2020"} 108 | 109 | iex> use CalendarInterval 110 | iex> #{inspect(__MODULE__)}.to_string ~I"2020-01/12" 111 | {:ok, "Jan 1 – Dec 31, 2020"} 112 | 113 | iex> #{inspect(__MODULE__)}.to_string Date.range(~D[2020-01-01], ~D[2020-01-12]), 114 | ...> format: :short 115 | {:ok, "1/1/2020 – 1/12/2020"} 116 | 117 | iex> #{inspect(__MODULE__)}.to_string Date.range(~D[2020-01-01], ~D[2020-01-12]), 118 | ...> format: :long, locale: "fr" 119 | {:ok, "mer. 1 – dim. 12 janv. 2020"} 120 | 121 | """ 122 | @spec to_string(Cldr.Interval.range(), Keyword.t()) :: 123 | {:ok, String.t()} | {:error, {module, String.t()}} 124 | 125 | if Cldr.Code.ensure_compiled?(CalendarInterval) do 126 | def to_string(%CalendarInterval{} = interval, options) do 127 | Cldr.Date.Interval.to_string(interval, unquote(backend), options) 128 | end 129 | end 130 | 131 | def to_string(%Elixir.Date.Range{} = interval, options) do 132 | Cldr.Date.Interval.to_string(interval, unquote(backend), options) 133 | end 134 | 135 | @doc false 136 | def to_string(unquote(date) = from, unquote(date) = to) do 137 | Cldr.Date.Interval.to_string(from, to, unquote(backend), []) 138 | end 139 | 140 | def to_string(nil = from, unquote(date) = to) do 141 | Cldr.Date.Interval.to_string(from, to, unquote(backend), []) 142 | end 143 | 144 | def to_string(unquote(date) = from, nil = to) do 145 | Cldr.Date.Interval.to_string(from, to, unquote(backend), []) 146 | end 147 | 148 | @doc """ 149 | Returns a interval formed from two dates as 150 | a localised string. 151 | 152 | ## Arguments 153 | 154 | * `from` is any map that conforms to the 155 | `Calendar.date` type. 156 | 157 | * `to` is any map that conforms to the 158 | `Calendar.date` type. `to` must occur 159 | on or after `from`. 160 | 161 | * `options` is a keyword list of options. The default is 162 | `[format: :medium, style: :date]`. 163 | 164 | Either `from` or `to` may also be `nil`, in which case an 165 | open interval is formatted and the non-nil item is formatted 166 | as a standalone date. 167 | 168 | ## Options 169 | 170 | * `:format` is one of `:short`, `:medium` or `:long` or a 171 | specific format type or a string representing of an interval 172 | format. The default is `:medium`. 173 | 174 | * `:style` supports different formatting styles. The 175 | alternatives are `:date`, `:month_and_day`, `:month` 176 | and `:year_and_month`. The default is `:date`. 177 | 178 | * `locale` is any valid locale name returned by `Cldr.known_locale_names/0` 179 | or a `t:Cldr.LanguageTag.t/0` struct. The default is `#{backend}.get_locale/0` 180 | 181 | * `number_system:` a number system into which the formatted date digits should 182 | be transliterated 183 | 184 | ## Returns 185 | 186 | * `{:ok, string}` or 187 | 188 | * `{:error, {exception, reason}}` 189 | 190 | ## Notes 191 | 192 | * For more information on interval format string 193 | see the `Cldr.Interval`. 194 | 195 | * The available predefined formats that can be applied are the 196 | keys of the map returned by `Cldr.DateTime.Format.interval_formats("en", :gregorian)` 197 | where `"en"` can be replaced by any configuration locale name and `:gregorian` 198 | is the underlying CLDR calendar type. 199 | 200 | * In the case where `from` and `to` are equal, a single 201 | date is formatted instead of an interval 202 | 203 | ## Examples 204 | 205 | iex> #{inspect(__MODULE__)}.to_string ~D[2020-01-01], ~D[2020-12-31] 206 | {:ok, "Jan 1 – Dec 31, 2020"} 207 | 208 | iex> #{inspect(__MODULE__)}.to_string ~D[2020-01-01], ~D[2020-01-12] 209 | {:ok, "Jan 1 – 12, 2020"} 210 | 211 | iex> #{inspect(__MODULE__)}.to_string ~D[2020-01-01], ~D[2020-01-12], 212 | ...> format: :long 213 | {:ok, "Wed, Jan 1 – Sun, Jan 12, 2020"} 214 | 215 | iex> #{inspect(__MODULE__)}.to_string ~D[2020-01-01], ~D[2020-12-01], 216 | ...> format: :long, style: :year_and_month 217 | {:ok, "January – December 2020"} 218 | 219 | iex> #{inspect(__MODULE__)}.to_string ~D[2020-01-01], ~D[2020-01-12], 220 | ...> format: :short 221 | {:ok, "1/1/2020 – 1/12/2020"} 222 | 223 | iex> #{inspect(__MODULE__)}.to_string ~D[2020-01-01], ~D[2020-01-12], 224 | ...> format: :long, locale: "fr" 225 | {:ok, "mer. 1 – dim. 12 janv. 2020"} 226 | 227 | iex> #{inspect(__MODULE__)}.to_string ~D[2020-01-01], ~D[2020-01-12], 228 | ...> format: :long, locale: "th", number_system: :thai 229 | {:ok, "พ. ๑ ม.ค. – อา. ๑๒ ม.ค. ๒๐๒๐"} 230 | 231 | """ 232 | @spec to_string(Elixir.Calendar.date() | nil, Elixir.Calendar.date() | nil, Keyword.t()) :: 233 | {:ok, String.t()} | {:error, {module, String.t()}} 234 | 235 | def to_string(unquote(date) = from, unquote(date) = to, options) do 236 | Cldr.Date.Interval.to_string(from, to, unquote(backend), options) 237 | end 238 | 239 | def to_string(nil = from, unquote(date) = to, options) do 240 | Cldr.Date.Interval.to_string(from, to, unquote(backend), options) 241 | end 242 | 243 | def to_string(unquote(date) = from, nil = to, options) do 244 | Cldr.Date.Interval.to_string(from, to, unquote(backend), options) 245 | end 246 | 247 | if Cldr.Code.ensure_compiled?(CalendarInterval) do 248 | @doc false 249 | def to_string!(%CalendarInterval{} = interval) do 250 | locale = unquote(backend).get_locale() 251 | Cldr.Date.Interval.to_string!(interval, unquote(backend), locale: locale) 252 | end 253 | end 254 | 255 | @doc false 256 | def to_string!(%Elixir.Date.Range{} = interval) do 257 | locale = unquote(backend).get_locale() 258 | Cldr.Date.Interval.to_string!(interval, unquote(backend), locale: locale) 259 | end 260 | 261 | @doc """ 262 | Returns a `Date.Range` or `CalendarInterval` as 263 | a localised string. 264 | 265 | ## Arguments 266 | 267 | * `range` as either a`Date.Range.t` returned from `Date.range/2` 268 | or a `CalendarInterval.t` 269 | 270 | * `options` is a keyword list of options. The default is 271 | `[format: :medium, style: :date]`. 272 | 273 | ## Options 274 | 275 | * `:format` is one of `:short`, `:medium` or `:long` or a 276 | specific format type or a string representing of an interval 277 | format. The default is `:medium`. 278 | 279 | * `:style` supports different formatting styles. The 280 | alternatives are `:date`, `:month_and_day`, `:month` 281 | and `:year_and_month`. The default is `:date`. 282 | 283 | * `locale` is any valid locale name returned by `Cldr.known_locale_names/0` 284 | or a `t:Cldr.LanguageTag.t/0` struct. The default is `#{backend}.get_locale/0` 285 | 286 | * `number_system:` a number system into which the formatted date digits should 287 | be transliterated 288 | 289 | ## Returns 290 | 291 | * `string` or 292 | 293 | * raises an exception 294 | 295 | ## Notes 296 | 297 | * `CalendarInterval` support requires adding the 298 | dependency [calendar_interval](https://hex.pm/packages/calendar_interval) 299 | to the `deps` configuration in `mix.exs`. 300 | 301 | * For more information on interval format string 302 | see the `Cldr.Interval`. 303 | 304 | * The available predefined formats that can be applied are the 305 | keys of the map returned by `Cldr.DateTime.Format.interval_formats("en", :gregorian)` 306 | where `"en"` can be replaced by any configuration locale name and `:gregorian` 307 | is the underlying CLDR calendar type. 308 | 309 | * In the case where `from` and `to` are equal, a single 310 | date is formatted instead of an interval 311 | 312 | ## Examples 313 | 314 | iex> #{inspect(__MODULE__)}.to_string! Date.range(~D[2020-01-01], ~D[2020-12-31]) 315 | "Jan 1 – Dec 31, 2020" 316 | 317 | iex> #{inspect(__MODULE__)}.to_string! Date.range(~D[2020-01-01], ~D[2020-01-12]) 318 | "Jan 1 – 12, 2020" 319 | 320 | iex> #{inspect(__MODULE__)}.to_string! Date.range(~D[2020-01-01], ~D[2020-01-12]), 321 | ...> format: :long 322 | "Wed, Jan 1 – Sun, Jan 12, 2020" 323 | 324 | iex> #{inspect(__MODULE__)}.to_string! Date.range(~D[2020-01-01], ~D[2020-12-01]), 325 | ...> format: :long, style: :year_and_month 326 | "January – December 2020" 327 | 328 | iex> use CalendarInterval 329 | iex> #{inspect(__MODULE__)}.to_string! ~I"2020-01/12" 330 | "Jan 1 – Dec 31, 2020" 331 | 332 | iex> #{inspect(__MODULE__)}.to_string! Date.range(~D[2020-01-01], ~D[2020-01-12]), 333 | ...> format: :short 334 | "1/1/2020 – 1/12/2020" 335 | 336 | iex> #{inspect(__MODULE__)}.to_string! Date.range(~D[2020-01-01], ~D[2020-01-12]), 337 | ...> format: :long, locale: "fr" 338 | "mer. 1 – dim. 12 janv. 2020" 339 | 340 | """ 341 | @spec to_string!(Cldr.Interval.range(), Keyword.t()) :: 342 | String.t() | no_return 343 | 344 | if Cldr.Code.ensure_compiled?(CalendarInterval) do 345 | def to_string!(%CalendarInterval{} = interval, options) do 346 | locale = unquote(backend).get_locale() 347 | options = Keyword.put_new(options, :locale, locale) 348 | Cldr.Date.Interval.to_string!(interval, unquote(backend), options) 349 | end 350 | end 351 | 352 | def to_string!(%Elixir.Date.Range{} = interval, options) do 353 | locale = unquote(backend).get_locale() 354 | options = Keyword.put_new(options, :locale, locale) 355 | Cldr.Date.Interval.to_string!(interval, unquote(backend), options) 356 | end 357 | 358 | @doc false 359 | def to_string!(from, to) do 360 | locale = unquote(backend).get_locale() 361 | Cldr.Date.Interval.to_string!(from, to, unquote(backend), locale: locale) 362 | end 363 | 364 | @doc """ 365 | Returns a interval formed from two dates as 366 | a localised string. 367 | 368 | ## Arguments 369 | 370 | * `from` is any map that conforms to the 371 | `Calendar.date` type. 372 | 373 | * `to` is any map that conforms to the 374 | `Calendar.date` type. `to` must occur 375 | on or after `from`. 376 | 377 | * `options` is a keyword list of options. The default is 378 | `[format: :medium, style: :date]`. 379 | 380 | Either `from` or `to` may also be `nil`, in which case an 381 | open interval is formatted and the non-nil item is formatted 382 | as a standalone date. 383 | 384 | ## Options 385 | 386 | * `:format` is one of `:short`, `:medium` or `:long` or a 387 | specific format type or a string representing of an interval 388 | format. The default is `:medium`. 389 | 390 | * `:style` supports different formatting styles. The 391 | alternatives are `:date`, `:month_and_day`, `:month` 392 | and `:year_and_month`. The default is `:date`. 393 | 394 | * `locale` is any valid locale name returned by `Cldr.known_locale_names/0` 395 | or a `t:Cldr.LanguageTag.t/0` struct. The default is `#{backend}.get_locale/0`. 396 | 397 | * `number_system:` a number system into which the formatted date digits should 398 | be transliterated. 399 | 400 | ## Returns 401 | 402 | * `string` or 403 | 404 | * raises an exception 405 | 406 | ## Notes 407 | 408 | * For more information on interval format string 409 | see the `Cldr.Interval`. 410 | 411 | * The available predefined formats that can be applied are the 412 | keys of the map returned by `Cldr.DateTime.Format.interval_formats("en", :gregorian)` 413 | where `"en"` can be replaced by any configuration locale name and `:gregorian` 414 | is the underlying CLDR calendar type. 415 | 416 | * In the case where `from` and `to` are equal, a single 417 | date is formatted instead of an interval 418 | 419 | ## Examples 420 | 421 | iex> #{inspect(__MODULE__)}.to_string! Date.range(~D[2020-01-01], ~D[2020-12-31]) 422 | "Jan 1 – Dec 31, 2020" 423 | 424 | iex> #{inspect(__MODULE__)}.to_string! Date.range(~D[2020-01-01], ~D[2020-01-12]) 425 | "Jan 1 – 12, 2020" 426 | 427 | iex> #{inspect(__MODULE__)}.to_string! Date.range(~D[2020-01-01], ~D[2020-01-12]), 428 | ...> format: :long 429 | "Wed, Jan 1 – Sun, Jan 12, 2020" 430 | 431 | iex> #{inspect(__MODULE__)}.to_string! Date.range(~D[2020-01-01], ~D[2020-12-01]), 432 | ...> format: :long, style: :year_and_month 433 | "January – December 2020" 434 | 435 | iex> use CalendarInterval 436 | iex> #{inspect(__MODULE__)}.to_string! ~I"2020-01/12" 437 | "Jan 1 – Dec 31, 2020" 438 | 439 | iex> #{inspect(__MODULE__)}.to_string! Date.range(~D[2020-01-01], ~D[2020-01-12]), 440 | ...> format: :short 441 | "1/1/2020 – 1/12/2020" 442 | 443 | iex> #{inspect(__MODULE__)}.to_string! Date.range(~D[2020-01-01], ~D[2020-01-12]), 444 | ...> format: :long, locale: "fr" 445 | "mer. 1 – dim. 12 janv. 2020" 446 | 447 | """ 448 | @spec to_string!(Elixir.Calendar.date() | nil, Elixir.Calendar.date() | nil, Keyword.t()) :: 449 | String.t() | no_return 450 | 451 | def to_string!(unquote(date) = from, unquote(date) = to, options) do 452 | do_to_string!(from, to, options) 453 | end 454 | 455 | def to_string!(nil = from, unquote(date) = to, options) do 456 | do_to_string!(from, to, options) 457 | end 458 | 459 | def to_string!(unquote(date) = from, nil = to, options) do 460 | do_to_string!(from, to, options) 461 | end 462 | 463 | def do_to_string!(from, to, options) do 464 | locale = unquote(backend).get_locale() 465 | options = Keyword.put_new(options, :locale, locale) 466 | Cldr.Date.Interval.to_string!(from, to, unquote(backend), options) 467 | end 468 | end 469 | end 470 | end 471 | end 472 | -------------------------------------------------------------------------------- /lib/cldr/backend/interval/date_time.ex: -------------------------------------------------------------------------------- 1 | defmodule Cldr.DateTime.Interval.Backend do 2 | @moduledoc false 3 | 4 | def define_date_time_interval_module(config) do 5 | backend = config.backend 6 | config = Macro.escape(config) 7 | 8 | quote location: :keep, bind_quoted: [config: config, backend: backend] do 9 | defmodule DateTime.Interval do 10 | @moduledoc """ 11 | Interval formats allow for software to format intervals like "Jan 10-12, 2008" as a 12 | shorter and more natural format than "Jan 10, 2008 - Jan 12, 2008". They are designed 13 | to take a start and end date, time or datetime plus a formatting pattern 14 | and use that information to produce a localized format. 15 | 16 | See `Cldr.Interval.to_string/3` and `Cldr.DateTime.Interval.to_string/3` 17 | 18 | """ 19 | 20 | naivedatetime = 21 | quote do 22 | %{ 23 | year: _, 24 | month: _, 25 | day: _, 26 | hour: _, 27 | minute: _, 28 | second: _, 29 | microsecond: _, 30 | calendar: var!(calendar, unquote(__MODULE__)) 31 | } 32 | end 33 | 34 | if Cldr.Code.ensure_compiled?(CalendarInterval) do 35 | @doc false 36 | def to_string(%CalendarInterval{} = interval) do 37 | locale = unquote(backend).get_locale() 38 | Cldr.DateTime.Interval.to_string(interval, unquote(backend), locale: locale) 39 | end 40 | 41 | @doc """ 42 | Returns a `CalendarInterval` as a localised 43 | datetime string. 44 | 45 | ## Arguments 46 | 47 | * `range` is a `CalendarInterval.t` 48 | 49 | * `options` is a keyword list of options. The default is 50 | `[format: :medium]`. 51 | 52 | ## Options 53 | 54 | * `:format` is one of `:short`, `:medium` or `:long` or a 55 | specific format type or a string representing of an interval 56 | format. The default is `:medium`. 57 | 58 | * `:locale` is any valid locale name returned by `Cldr.known_locale_names/0` 59 | or a `t:Cldr.LanguageTag.t/0` struct. The default is `#{backend}.get_locale/0` 60 | 61 | * `:number_system` a number system into which the formatted datetime 62 | digits should be transliterated 63 | 64 | ## Returns 65 | 66 | * `{:ok, string}` or 67 | 68 | * `{:error, {exception, reason}}` 69 | 70 | ## Notes 71 | 72 | * `CalendarInterval` support requires adding the 73 | dependency [calendar_interval](https://hex.pm/packages/calendar_interval) 74 | to the `deps` configuration in `mix.exs`. 75 | 76 | * For more information on interval format string 77 | see the `Cldr.Interval`. 78 | 79 | * The available predefined formats that can be applied are the 80 | keys of the map returned by `Cldr.DateTime.Format.interval_formats("en", :gregorian)` 81 | where `"en"` can be replaced by any configuration locale name and `:gregorian` 82 | is the underlying CLDR calendar type. 83 | 84 | * In the case where `from` and `to` are equal, a single 85 | date is formatted instead of an interval 86 | 87 | ## Examples 88 | 89 | iex> use CalendarInterval 90 | iex> #{inspect(__MODULE__)}.to_string ~I"2020-01-01 00:00/10:00" 91 | {:ok, "Jan 1, 2020, 12:00:00 AM – 10:00:00 AM"} 92 | 93 | """ 94 | 95 | @spec to_string(CalendarInterval.t(), Keyword.t()) :: 96 | {:ok, String.t()} | {:error, {module, String.t()}} 97 | 98 | def to_string(%CalendarInterval{} = interval, options) do 99 | locale = unquote(backend).get_locale() 100 | options = Keyword.put_new(options, :locale, locale) 101 | Cldr.DateTime.Interval.to_string(interval, unquote(backend), options) 102 | end 103 | end 104 | 105 | @doc """ 106 | Returns a string representing the formatted 107 | interval formed by two dates. 108 | 109 | ## Arguments 110 | 111 | * `from` is any map that conforms to the 112 | `Calendar.datetime` type. 113 | 114 | * `to` is any map that conforms to the 115 | `Calendar.datetime` type. `to` must occur 116 | on or after `from`. 117 | 118 | * `options` is a keyword list of options. The default is 119 | `[format: :medium]`. 120 | 121 | Either `from` or `to` may also be `nil`, in which case an 122 | open interval is formatted and the non-nil item is formatted 123 | as a standalone datetime. 124 | 125 | ## Options 126 | 127 | * `:format` is one of `:short`, `:medium` or `:long` or a 128 | specific format type or a string representing of an interval 129 | format. The default is `:medium`. 130 | 131 | * `locale` is any valid locale name returned by `Cldr.known_locale_names/0` 132 | or a `t:Cldr.LanguageTag.t/0` struct. The default is `#{backend}.get_locale/0` 133 | 134 | * `number_system:` a number system into which the formatted date digits should 135 | be transliterated 136 | 137 | ## Returns 138 | 139 | * `{:ok, string}` or 140 | 141 | * `{:error, {exception, reason}}` 142 | 143 | ## Notes 144 | 145 | * `CalendarInterval` support requires adding the 146 | dependency [calendar_interval](https://hex.pm/packages/calendar_interval) 147 | to the `deps` configuration in `mix.exs`. 148 | 149 | * For more information on interval format string 150 | see the `Cldr.Interval`. 151 | 152 | * The available predefined formats that can be applied are the 153 | keys of the map returned by `Cldr.DateTime.Format.interval_formats("en", :gregorian)` 154 | where `"en"` can be replaced by any configuration locale name and `:gregorian` 155 | is the underlying CLDR calendar type. 156 | 157 | * In the case where `from` and `to` are equal, a single 158 | date is formatted instead of an interval 159 | 160 | ## Examples 161 | 162 | iex> #{inspect(__MODULE__)}.to_string ~U[2020-01-01 00:00:00.0Z], 163 | ...> ~U[2020-12-31 10:00:00.0Z] 164 | {:ok, "Jan 1, 2020, 12:00:00 AM – Dec 31, 2020, 10:00:00 AM"} 165 | 166 | iex> #{inspect(__MODULE__)}.to_string ~U[2020-01-01 00:00:00.0Z], nil 167 | {:ok, "Jan 1, 2020, 12:00:00 AM –"} 168 | 169 | """ 170 | @spec to_string( 171 | Elixir.Calendar.naive_datetime() | nil, 172 | Elixir.Calendar.naive_datetime() | nil, 173 | Keyword.t() 174 | ) :: 175 | {:ok, String.t()} | {:error, {module, String.t()}} 176 | 177 | def to_string(from, to, options \\ []) 178 | 179 | def to_string(unquote(naivedatetime) = from, unquote(naivedatetime) = to, options) do 180 | do_to_string(from, to, options) 181 | end 182 | 183 | def to_string(nil = from, unquote(naivedatetime) = to, options) do 184 | do_to_string(from, to, options) 185 | end 186 | 187 | def to_string(unquote(naivedatetime) = from, nil = to, options) do 188 | do_to_string(from, to, options) 189 | end 190 | 191 | def do_to_string(from, to, options) do 192 | locale = unquote(backend).get_locale() 193 | options = Keyword.put_new(options, :locale, locale) 194 | Cldr.DateTime.Interval.to_string(from, to, unquote(backend), options) 195 | end 196 | 197 | if Cldr.Code.ensure_compiled?(CalendarInterval) do 198 | @doc false 199 | def to_string!(%CalendarInterval{} = interval) do 200 | locale = unquote(backend).get_locale() 201 | Cldr.DateTime.Interval.to_string!(interval, unquote(backend), locale: locale) 202 | end 203 | 204 | @doc """ 205 | Returns a `CalendarInterval` as a localised 206 | datetime string or raises an exception. 207 | 208 | ## Arguments 209 | 210 | * `range` is a `CalendarInterval.t` 211 | 212 | * `options` is a keyword list of options. The default is 213 | `[format: :medium]`. 214 | 215 | ## Options 216 | 217 | * `:format` is one of `:short`, `:medium` or `:long` or a 218 | specific format type or a string representing of an interval 219 | format. The default is `:medium`. 220 | 221 | * `:locale` is any valid locale name returned by `Cldr.known_locale_names/0` 222 | or a `t:Cldr.LanguageTag.t/0` struct. The default is `#{backend}.get_locale/0` 223 | 224 | * `:number_system` a number system into which the formatted datetime 225 | digits should be transliterated 226 | 227 | ## Returns 228 | 229 | * `string` or 230 | 231 | * raises an exception 232 | 233 | ## Notes 234 | 235 | * `CalendarInterval` support requires adding the 236 | dependency [calendar_interval](https://hex.pm/packages/calendar_interval) 237 | to the `deps` configuration in `mix.exs`. 238 | 239 | * For more information on interval format string 240 | see the `Cldr.Interval`. 241 | 242 | * The available predefined formats that can be applied are the 243 | keys of the map returned by `Cldr.DateTime.Format.interval_formats("en", :gregorian)` 244 | where `"en"` can be replaced by any configuration locale name and `:gregorian` 245 | is the underlying CLDR calendar type. 246 | 247 | * In the case where `from` and `to` are equal, a single 248 | date is formatted instead of an interval 249 | 250 | ## Examples 251 | 252 | iex> use CalendarInterval 253 | iex> #{inspect(__MODULE__)}.to_string! ~I"2020-01-01 00:00/10:00" 254 | "Jan 1, 2020, 12:00:00 AM – 10:00:59 AM" 255 | 256 | """ 257 | 258 | @spec to_string!(CalendarInterval.t(), Keyword.t()) :: 259 | String.t() | no_return 260 | 261 | def to_string!(%CalendarInterval{} = interval, options) do 262 | locale = unquote(backend).get_locale() 263 | options = Keyword.put_new(options, :locale, locale) 264 | Cldr.DateTime.Interval.to_string!(interval, unquote(backend), options) 265 | end 266 | end 267 | 268 | @doc """ 269 | Returns a string representing the formatted 270 | interval formed by two dates or raises an 271 | exception. 272 | 273 | ## Arguments 274 | 275 | * `from` is any map that conforms to the 276 | `Calendar.datetime` type. 277 | 278 | * `to` is any map that conforms to the 279 | `Calendar.datetime` type. `to` must occur 280 | on or after `from`. 281 | 282 | * `options` is a keyword list of options. The default is 283 | `[format: :medium]`. 284 | 285 | Either `from` or `to` may also be `nil`, in which case an 286 | open interval is formatted and the non-nil item is formatted 287 | as a standalone datetime. 288 | 289 | ## Options 290 | 291 | * `:format` is one of `:short`, `:medium` or `:long` or a 292 | specific format type or a string representing of an interval 293 | format. The default is `:medium`. 294 | 295 | * `locale` is any valid locale name returned by `Cldr.known_locale_names/0` 296 | or a `t:Cldr.LanguageTag.t/0` struct. The default is `#{backend}.get_locale/0`. 297 | 298 | * `number_system:` a number system into which the formatted date digits should 299 | be transliterated. 300 | 301 | ## Returns 302 | 303 | * `string` or 304 | 305 | * raises an exception 306 | 307 | ## Notes 308 | 309 | * For more information on interval format string 310 | see the `Cldr.Interval`. 311 | 312 | * The available predefined formats that can be applied are the 313 | keys of the map returned by `Cldr.DateTime.Format.interval_formats("en", :gregorian)` 314 | where `"en"` can be replaced by any configuration locale name and `:gregorian` 315 | is the underlying CLDR calendar type. 316 | 317 | * In the case where `from` and `to` are equal, a single 318 | date is formatted instead of an interval. 319 | 320 | ## Examples 321 | 322 | iex> #{inspect(__MODULE__)}.to_string! ~U[2020-01-01 00:00:00.0Z], 323 | ...> ~U[2020-12-31 10:00:00.0Z] 324 | "Jan 1, 2020, 12:00:00 AM – Dec 31, 2020, 10:00:00 AM" 325 | 326 | """ 327 | @spec to_string!( 328 | Elixir.Calendar.naive_datetime() | nil, 329 | Elixir.Calendar.naive_datetime() | nil, 330 | Keyword.t() 331 | ) :: 332 | String.t() | no_return() 333 | 334 | def to_string!(from, to, options \\ []) 335 | 336 | def to_string!(unquote(naivedatetime) = from, unquote(naivedatetime) = to, options) do 337 | do_to_string!(from, to, options) 338 | end 339 | 340 | def to_string!(nil = from, unquote(naivedatetime) = to, options) do 341 | do_to_string!(from, to, options) 342 | end 343 | 344 | def to_string!(unquote(naivedatetime) = from, nil = to, options) do 345 | do_to_string!(from, to, options) 346 | end 347 | 348 | def do_to_string!(from, to, options) do 349 | locale = unquote(backend).get_locale() 350 | options = Keyword.put_new(options, :locale, locale) 351 | Cldr.DateTime.Interval.to_string!(from, to, unquote(backend), options) 352 | end 353 | end 354 | end 355 | end 356 | end 357 | -------------------------------------------------------------------------------- /lib/cldr/backend/interval/time.ex: -------------------------------------------------------------------------------- 1 | defmodule Cldr.Time.Interval.Backend do 2 | @moduledoc false 3 | 4 | def define_time_interval_module(config) do 5 | backend = config.backend 6 | config = Macro.escape(config) 7 | 8 | quote location: :keep, bind_quoted: [config: config, backend: backend] do 9 | defmodule Time.Interval do 10 | @moduledoc """ 11 | Interval formats allow for software to format intervals like "Jan 10-12, 2008" as a 12 | shorter and more natural format than "Jan 10, 2008 - Jan 12, 2008". They are designed 13 | to take a start and end date, time or datetime plus a formatting pattern 14 | and use that information to produce a localized format. 15 | 16 | See `Cldr.Interval.to_string/3` and `Cldr.Time.Interval.to_string/3`. 17 | 18 | """ 19 | 20 | import Cldr.Calendar, 21 | only: [ 22 | time: 0 23 | ] 24 | 25 | @doc """ 26 | Returns a string representing the formatted 27 | interval formed by two times. 28 | 29 | ## Arguments 30 | 31 | * `from` is any map that conforms to the 32 | `Calendar.time` type. 33 | 34 | * `to` is any map that conforms to the 35 | `Calendar.time` type. `to` must occur 36 | on or after `from`. 37 | 38 | * `options` is a keyword list of options. The default is 39 | `[format: :medium, style: :time]`. 40 | 41 | Either `from` or `to` may also be `nil`, in which case an 42 | open interval is formatted and the non-nil item is formatted 43 | as a standalone time. 44 | 45 | ## Options 46 | 47 | * `:format` is one of `:short`, `:medium` or `:long` or a 48 | specific format type or a string representing of an interval 49 | format. The default is `:medium`. 50 | 51 | * `:style` supports different formatting styles. The 52 | alternatives are `:time`, `:zone`, 53 | and `:flex`. The default is `:time`. 54 | 55 | * `locale` is any valid locale name returned by `Cldr.known_locale_names/0` 56 | or a `t:Cldr.LanguageTag.t/0` struct. The default is `#{backend}.get_locale/0`. 57 | 58 | * `number_system:` a number system into which the formatted date digits should 59 | be transliterated. 60 | 61 | ## Returns 62 | 63 | * `{:ok, string}` or 64 | 65 | * `{:error, {exception, reason}}` 66 | 67 | ## Notes 68 | 69 | * For more information on interval format string 70 | see `Cldr.Interval`. 71 | 72 | * The available predefined formats that can be applied are the 73 | keys of the map returned by `Cldr.DateTime.Format.interval_formats("en", :gregorian)` 74 | where `"en"` can be replaced by any configured locale name and `:gregorian` 75 | is the underlying CLDR calendar type. 76 | 77 | * In the case where `from` and `to` are equal, a single 78 | time is formatted instead of an interval. 79 | 80 | ## Examples 81 | 82 | iex> #{inspect(__MODULE__)}.to_string ~T[10:00:00], ~T[10:03:00], format: :short 83 | {:ok, "10 – 10 AM"} 84 | 85 | iex> #{inspect(__MODULE__)}.to_string ~T[10:00:00], ~T[10:03:00], format: :medium 86 | {:ok, "10:00 – 10:03 AM"} 87 | 88 | iex> #{inspect(__MODULE__)}.to_string ~T[10:00:00], ~T[10:03:00], format: :long 89 | {:ok, "10:00 – 10:03 AM"} 90 | 91 | iex> #{inspect(__MODULE__)}.to_string ~T[10:00:00], ~T[10:03:00], 92 | ...> format: :long, style: :flex 93 | {:ok, "10:00 – 10:03 in the morning"} 94 | 95 | iex> #{inspect(__MODULE__)}.to_string ~U[2020-01-01 00:00:00.0Z], ~U[2020-01-01 10:00:00.0Z], 96 | ...> format: :long, style: :flex 97 | {:ok, "12:00 – 10:00 in the morning"} 98 | 99 | iex> #{inspect(__MODULE__)}.to_string ~U[2020-01-01 00:00:00.0Z], ~U[2020-01-01 10:00:00.0Z], 100 | ...> format: :long, style: :zone 101 | {:ok, "12:00 – 10:00 AM Etc/UTC"} 102 | 103 | iex> #{inspect(__MODULE__)}.to_string ~T[10:00:00], ~T[10:03:00], 104 | ...> format: :long, style: :flex, locale: "th" 105 | {:ok, "10:00 – 10:03 ในตอนเช้า"} 106 | 107 | iex> #{inspect(__MODULE__)}.to_string ~T[10:00:00], nil 108 | {:ok, "10:00:00 AM –"} 109 | 110 | """ 111 | @spec to_string(Elixir.Calendar.time() | nil, Elixir.Calendar.time() | nil, Keyword.t()) :: 112 | {:ok, String.t()} | {:error, {module, String.t()}} 113 | 114 | def to_string(from, to, options \\ []) 115 | 116 | def to_string(unquote(time()) = from, unquote(time()) = to, options) do 117 | do_to_string(from, to, options) 118 | end 119 | 120 | def to_string(nil = from, unquote(time()) = to, options) do 121 | do_to_string(from, to, options) 122 | end 123 | 124 | def to_string(unquote(time()) = from, nil = to, options) do 125 | do_to_string(from, to, options) 126 | end 127 | 128 | def do_to_string(from, to, options) do 129 | locale = unquote(backend).get_locale() 130 | options = Keyword.put_new(options, :locale, locale) 131 | Cldr.Time.Interval.to_string(from, to, unquote(backend), options) 132 | end 133 | 134 | @doc """ 135 | Returns a string representing the formatted 136 | interval formed by two times or raises an 137 | exception. 138 | 139 | ## Arguments 140 | 141 | * `from` is any map that conforms to the 142 | `Calendar.time` type. 143 | 144 | * `to` is any map that conforms to the 145 | `Calendar.time` type. `to` must occur 146 | on or after `from`. 147 | 148 | * `options` is a keyword list of options. The default is 149 | `[format: :medium, style: :time]`. 150 | 151 | Either `from` or `to` may also be `nil`, in which case an 152 | open interval is formatted and the non-nil item is formatted 153 | as a standalone time. 154 | 155 | ## Options 156 | 157 | * `:format` is one of `:short`, `:medium` or `:long` or a 158 | specific format type or a string representing of an interval 159 | format. The default is `:medium`. 160 | 161 | * `:style` supports different formatting styles. The 162 | alternatives are `:time`, `:zone`, 163 | and `:flex`. The default is `:time`. 164 | 165 | * `locale` is any valid locale name returned by `Cldr.known_locale_names/0` 166 | or a `t:Cldr.LanguageTag.t/0` struct. The default is `#{backend}.get_locale/0`. 167 | 168 | * `number_system:` a number system into which the formatted date digits should 169 | be transliterated. 170 | 171 | ## Returns 172 | 173 | * `string` or 174 | 175 | * raises an exception 176 | 177 | ## Notes 178 | 179 | * For more information on interval format string 180 | see `Cldr.Interval`. 181 | 182 | * The available predefined formats that can be applied are the 183 | keys of the map returned by `Cldr.DateTime.Format.interval_formats("en", :gregorian)` 184 | where `"en"` can be replaced by any configured locale name and `:gregorian` 185 | is the underlying CLDR calendar type. 186 | 187 | * In the case where `from` and `to` are equal, a single 188 | time is formatted instead of an interval. 189 | 190 | ## Examples 191 | 192 | iex> #{inspect(__MODULE__)}.to_string! ~T[10:00:00], ~T[10:03:00], format: :short 193 | "10 – 10 AM" 194 | 195 | iex> #{inspect(__MODULE__)}.to_string! ~T[10:00:00], ~T[10:03:00], format: :medium 196 | "10:00 – 10:03 AM" 197 | 198 | iex> #{inspect(__MODULE__)}.to_string! ~T[10:00:00], ~T[10:03:00], format: :long 199 | "10:00 – 10:03 AM" 200 | 201 | iex> #{inspect(__MODULE__)}.to_string! ~T[10:00:00], ~T[10:03:00], 202 | ...> format: :long, style: :flex 203 | "10:00 – 10:03 in the morning" 204 | 205 | iex> #{inspect(__MODULE__)}.to_string! ~U[2020-01-01 00:00:00.0Z], ~U[2020-01-01 10:00:00.0Z], 206 | ...> format: :long, style: :flex 207 | "12:00 – 10:00 in the morning" 208 | 209 | iex> #{inspect(__MODULE__)}.to_string! ~U[2020-01-01 00:00:00.0Z], ~U[2020-01-01 10:00:00.0Z], 210 | ...> format: :long, style: :zone 211 | "12:00 – 10:00 AM Etc/UTC" 212 | 213 | iex> #{inspect(__MODULE__)}.to_string! ~T[10:00:00], ~T[10:03:00], 214 | ...> format: :long, style: :flex, locale: "th" 215 | "10:00 – 10:03 ในตอนเช้า" 216 | 217 | """ 218 | @spec to_string!(Elixir.Calendar.time() | nil, Elixir.Calendar.time() | nil, Keyword.t()) :: 219 | String.t() | no_return() 220 | 221 | def to_string!(from, to, options \\ []) 222 | 223 | def to_string!(unquote(time()) = from, unquote(time()) = to, options) do 224 | do_to_string!(from, to, options) 225 | end 226 | 227 | def to_string!(nil = from, unquote(time()) = to, options) do 228 | do_to_string!(from, to, options) 229 | end 230 | 231 | def to_string!(unquote(time()) = from, nil = to, options) do 232 | do_to_string!(from, to, options) 233 | end 234 | 235 | def do_to_string!(from, to, options) do 236 | locale = unquote(backend).get_locale() 237 | options = Keyword.put_new(options, :locale, locale) 238 | Cldr.Time.Interval.to_string!(from, to, unquote(backend), options) 239 | end 240 | end 241 | end 242 | end 243 | end 244 | -------------------------------------------------------------------------------- /lib/cldr/backend/relative.ex: -------------------------------------------------------------------------------- 1 | defmodule Cldr.DateTime.Relative.Backend do 2 | @moduledoc false 3 | 4 | def define_date_time_relative_module(config) do 5 | backend = config.backend 6 | config = Macro.escape(config) 7 | 8 | quote location: :keep, bind_quoted: [config: config, backend: backend] do 9 | defmodule DateTime.Relative do 10 | @second 1 11 | @minute 60 12 | @hour 3600 13 | @day 86400 14 | @week 604_800 15 | @month 2_629_743.83 16 | @year 31_556_926 17 | 18 | @unit %{ 19 | second: @second, 20 | minute: @minute, 21 | hour: @hour, 22 | day: @day, 23 | week: @week, 24 | month: @month, 25 | year: @year 26 | } 27 | 28 | @other_units [:mon, :tue, :wed, :thu, :fri, :sat, :sun, :quarter] 29 | @unit_keys Enum.sort(Map.keys(@unit) ++ @other_units) 30 | 31 | @doc false 32 | def get_locale(locale \\ unquote(backend).get_locale()) 33 | 34 | def get_locale(locale_name) when is_binary(locale_name) do 35 | with {:ok, locale} <- Cldr.validate_locale(locale_name, unquote(backend)) do 36 | get_locale(locale) 37 | end 38 | end 39 | 40 | @doc """ 41 | Returns a `{:ok, string}` representing a relative time (ago, in) for a given 42 | number, `t:Date.t/0` or `t:Datetime.t/0`. Returns `{:error, reason}` when errors 43 | are detected. 44 | 45 | * `relative` is a number or Date/Datetime representing the time distance from `now` or from 46 | options[:relative_to] 47 | 48 | * `options` is a `Keyword` list of options which are: 49 | 50 | ## Options 51 | 52 | * `:locale` is the locale in which the binary is formatted. 53 | The default is `Cldr.get_locale/0` 54 | 55 | * `:format` is the format of the binary. Style may be `:default`, `:narrow` or `:short` 56 | 57 | * `:unit` is the time unit for the formatting. The allowable units are `:second`, `:minute`, 58 | `:hour`, `:day`, `:week`, `:month`, `:year`, `:mon`, `:tue`, `:wed`, `:thu`, `:fri`, `:sat`, 59 | `:sun`, `:quarter` 60 | 61 | * `:relative_to` is the baseline Date or Datetime from which the difference from `relative` is 62 | calculated when `relative` is a Date or a DateTime. The default for a Date is `Date.utc_today`, 63 | for a DateTime it is `DateTime.utc_now` 64 | 65 | ### Notes 66 | 67 | When `options[:unit]` is not specified, `MyApp.Cldr.DateTime.Relative.to_string/2` 68 | attempts to identify the appropriate unit based upon the magnitude of `relative`. 69 | For example, given a parameter of less than `60`, then `to_string/2` will 70 | assume `:seconds` as the unit. See `unit_from_relative_time/1`. 71 | 72 | ## Examples 73 | 74 | iex> #{inspect(__MODULE__)}.to_string(-1) 75 | {:ok, "1 second ago"} 76 | 77 | iex> #{inspect(__MODULE__)}.to_string(1) 78 | {:ok, "in 1 second"} 79 | 80 | iex> #{inspect(__MODULE__)}.to_string(1, unit: :day) 81 | {:ok, "tomorrow"} 82 | 83 | iex> #{inspect(__MODULE__)}.to_string(1, unit: :day, locale: "fr") 84 | {:ok, "demain"} 85 | 86 | iex> #{inspect(__MODULE__)}.to_string(1, unit: :day, format: :narrow) 87 | {:ok, "tomorrow"} 88 | 89 | iex> #{inspect(__MODULE__)}.to_string(1234, unit: :year) 90 | {:ok, "in 1,234 years"} 91 | 92 | iex> #{inspect(__MODULE__)}.to_string(1234, unit: :year, locale: "fr") 93 | {:ok, "dans 1 234 ans"} 94 | 95 | iex> #{inspect(__MODULE__)}.to_string(31) 96 | {:ok, "in 31 seconds"} 97 | 98 | iex> #{inspect(__MODULE__)}.to_string(~D[2017-04-29], relative_to: ~D[2017-04-26]) 99 | {:ok, "in 3 days"} 100 | 101 | iex> #{inspect(__MODULE__)}.to_string(310, format: :short, locale: "fr") 102 | {:ok, "dans 5 min"} 103 | 104 | iex> #{inspect(__MODULE__)}.to_string(310, format: :narrow, locale: "fr") 105 | {:ok, "+5 min"} 106 | 107 | iex> #{inspect(__MODULE__)}.to_string 2, unit: :wed, format: :short, locale: "en" 108 | {:ok, "in 2 Wed."} 109 | 110 | iex> #{inspect(__MODULE__)}.to_string 1, unit: :wed, format: :short 111 | {:ok, "next Wed."} 112 | 113 | iex> #{inspect(__MODULE__)}.to_string -1, unit: :wed, format: :short 114 | {:ok, "last Wed."} 115 | 116 | iex> #{inspect(__MODULE__)}.to_string -1, unit: :wed 117 | {:ok, "last Wednesday"} 118 | 119 | iex> #{inspect(__MODULE__)}.to_string -1, unit: :quarter 120 | {:ok, "last quarter"} 121 | 122 | iex> #{inspect(__MODULE__)}.to_string -1, unit: :mon, locale: "fr" 123 | {:ok, "lundi dernier"} 124 | 125 | iex> #{inspect(__MODULE__)}.to_string(~D[2017-04-29], unit: :ziggeraut) 126 | {:error, {Cldr.DateTime.UnknownTimeUnit, 127 | "Unknown time unit :ziggeraut. Valid time units are [:day, :fri, :hour, :minute, :mon, :month, :quarter, :sat, :second, :sun, :thu, :tue, :wed, :week, :year]"}} 128 | 129 | """ 130 | 131 | @spec to_string(number | Elixir.Date.t() | Elixir.DateTime.t(), Keyword.t()) :: 132 | {:ok, String.t()} | {:error, {module, String.t()}} 133 | 134 | def to_string(time, options \\ []) do 135 | Cldr.DateTime.Relative.to_string(time, unquote(backend), options) 136 | end 137 | 138 | @doc """ 139 | Returns a `{:ok, string}` representing a relative time (ago, in) for a given 140 | number, Date or Datetime or raises an exception on error. 141 | 142 | ## Arguments 143 | 144 | * `relative` is a number or Date/Datetime representing the time distance from `now` or from 145 | options[:relative_to]. 146 | 147 | * `options` is a `Keyword` list of options. 148 | 149 | ## Options 150 | 151 | * `:locale` is the locale in which the binary is formatted. 152 | The default is `Cldr.get_locale/0` 153 | 154 | * `:format` is the format of the binary. Style may be `:default`, `:narrow` or `:short`. 155 | The default is `:default` 156 | 157 | * `:unit` is the time unit for the formatting. The allowable units are `:second`, `:minute`, 158 | `:hour`, `:day`, `:week`, `:month`, `:year`, `:mon`, `:tue`, `:wed`, `:thu`, `:fri`, `:sat`, 159 | `:sun`, `:quarter` 160 | 161 | * `:relative_to` is the baseline Date or Datetime from which the difference from `relative` is 162 | calculated when `relative` is a Date or a DateTime. The default for a Date is `Date.utc_today`, 163 | for a DateTime it is `DateTime.utc_now` 164 | 165 | See `to_string/2` 166 | 167 | """ 168 | @spec to_string!(number | Elixir.Date.t() | Elixir.DateTime.t(), Keyword.t()) :: String.t() 169 | def to_string!(time, options \\ []) do 170 | Cldr.DateTime.Relative.to_string!(time, unquote(backend), options) 171 | end 172 | 173 | for locale_name <- Cldr.Locale.Loader.known_locale_names(config) do 174 | locale_data = 175 | locale_name 176 | |> Cldr.Locale.Loader.get_locale(config) 177 | |> Map.get(:date_fields) 178 | |> Map.take(@unit_keys) 179 | 180 | def get_locale(%LanguageTag{cldr_locale_name: unquote(locale_name)}), 181 | do: unquote(Macro.escape(locale_data)) 182 | end 183 | end 184 | end 185 | end 186 | end 187 | -------------------------------------------------------------------------------- /lib/cldr/datetime/exception.ex: -------------------------------------------------------------------------------- 1 | defmodule Cldr.DateTime.UnknownTimeUnit do 2 | @moduledoc """ 3 | Exception raised when an attempt is made to use a time unit that is not known. 4 | in `Cldr.DateTime.Relative`. 5 | """ 6 | defexception [:message] 7 | 8 | def exception(message) do 9 | %__MODULE__{message: message} 10 | end 11 | end 12 | 13 | defmodule Cldr.DateTime.Compiler.ParseError do 14 | @moduledoc """ 15 | Exception raised when tokenizing a datetime format. 16 | """ 17 | defexception [:message] 18 | 19 | def exception(message) do 20 | %__MODULE__{message: message} 21 | end 22 | end 23 | 24 | defmodule Cldr.DateTime.UnresolvedFormat do 25 | @moduledoc """ 26 | Exception raised when formatting and there is no 27 | data for the given format. 28 | """ 29 | defexception [:message] 30 | 31 | def exception(message) do 32 | %__MODULE__{message: message} 33 | end 34 | end 35 | 36 | defmodule Cldr.DateTime.InvalidFormat do 37 | @moduledoc """ 38 | Exception raised when formatting and there is no 39 | data for the given format. 40 | """ 41 | defexception [:message] 42 | 43 | def exception(message) do 44 | %__MODULE__{message: message} 45 | end 46 | end 47 | 48 | defmodule Cldr.DateTime.FormatError do 49 | @moduledoc """ 50 | Exception raised when attempting to 51 | format a date or time which does not have 52 | the data available to fulfill the format. 53 | """ 54 | defexception [:message] 55 | 56 | def exception(message) do 57 | %__MODULE__{message: message} 58 | end 59 | end 60 | 61 | defmodule Cldr.DateTime.IntervalFormatError do 62 | @moduledoc """ 63 | Exception raised when attempting to 64 | compile an interval format. 65 | """ 66 | defexception [:message] 67 | 68 | def exception(message) do 69 | %__MODULE__{message: message} 70 | end 71 | end 72 | 73 | defmodule Cldr.DateTime.DateTimeOrderError do 74 | @moduledoc """ 75 | Exception raised when the first 76 | datetime in an interval is greater than 77 | the last datetime. 78 | """ 79 | defexception [:message] 80 | 81 | def exception(message) do 82 | %__MODULE__{message: message} 83 | end 84 | end 85 | 86 | defmodule Cldr.DateTime.IncompatibleTimeZonerError do 87 | @moduledoc """ 88 | Exception raised when the two 89 | datetimes are in different time zones 90 | """ 91 | defexception [:message] 92 | 93 | def exception(message) do 94 | %__MODULE__{message: message} 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /lib/cldr/datetime/relative.ex: -------------------------------------------------------------------------------- 1 | defmodule Cldr.DateTime.Relative do 2 | @moduledoc """ 3 | Functions to support the string formatting of relative time/datetime numbers. 4 | 5 | This module provides formatting of numbers (as integers, floats, Dates or DateTimes) 6 | as "ago" or "in" with an appropriate time unit. For example, "2 days ago" or 7 | "in 10 seconds" 8 | 9 | """ 10 | 11 | @second 1 12 | @minute 60 13 | @hour 3600 14 | @day 86400 15 | @week 604_800 16 | @month 2_629_743.83 17 | @year 31_556_926 18 | 19 | @unit %{ 20 | second: @second, 21 | minute: @minute, 22 | hour: @hour, 23 | day: @day, 24 | week: @week, 25 | month: @month, 26 | year: @year 27 | } 28 | 29 | @other_units [:mon, :tue, :wed, :thu, :fri, :sat, :sun, :quarter] 30 | @unit_keys Enum.sort(Map.keys(@unit) ++ @other_units) 31 | @known_styles [:default, :narrow, :short] 32 | 33 | @doc """ 34 | Returns a `{:ok, string}` representing a relative time (ago, in) for a given 35 | number, Date or Datetime. Returns `{:error, reason}` when errors are detected. 36 | 37 | * `relative` is a number or Date/Datetime representing the time distance from `now` or from 38 | `options[:relative_to]` 39 | 40 | * `backend` is any module that includes `use Cldr` and therefore 41 | is a `Cldr` backend module. The default is `Cldr.default_backend/0`. 42 | 43 | * `options` is a `Keyword` list of options which are: 44 | 45 | ## Options 46 | 47 | * `:locale` is the locale in which the binary is formatted. 48 | The default is `Cldr.get_locale/0` 49 | 50 | * `:format` is the format of the binary. Format may be `:default`, `:narrow` or `:short`. 51 | 52 | * `:unit` is the time unit for the formatting. The allowable units are `:second`, `:minute`, 53 | `:hour`, `:day`, `:week`, `:month`, `:year`, `:mon`, `:tue`, `:wed`, `:thu`, `:fri`, `:sat`, 54 | `:sun`, `:quarter`. 55 | 56 | * `:relative_to` is the baseline `t:Date/0` or `t:Datetime.t/0` from which the difference 57 | from `relative` is calculated when `relative` is a Date or a DateTime. The default for 58 | a `t:Date.t/0` is `Date.utc_today/0`, for a `t:DateTime.t/0` it is `DateTime.utc_now/0`. 59 | 60 | ### Notes 61 | 62 | When `options[:unit]` is not specified, `Cldr.DateTime.Relative.to_string/2` 63 | attempts to identify the appropriate unit based upon the magnitude of `relative`. 64 | 65 | For example, given a parameter of less than `60`, then `to_string/2` will assume 66 | `:seconds` as the unit. See `unit_from_relative_time/1`. 67 | 68 | ## Examples 69 | 70 | iex> Cldr.DateTime.Relative.to_string(-1, MyApp.Cldr) 71 | {:ok, "1 second ago"} 72 | 73 | iex> Cldr.DateTime.Relative.to_string(1, MyApp.Cldr) 74 | {:ok, "in 1 second"} 75 | 76 | iex> Cldr.DateTime.Relative.to_string(1, MyApp.Cldr, unit: :day) 77 | {:ok, "tomorrow"} 78 | 79 | iex> Cldr.DateTime.Relative.to_string(1, MyApp.Cldr, unit: :day, locale: "fr") 80 | {:ok, "demain"} 81 | 82 | iex> Cldr.DateTime.Relative.to_string(1, MyApp.Cldr, unit: :day, format: :narrow) 83 | {:ok, "tomorrow"} 84 | 85 | iex> Cldr.DateTime.Relative.to_string(1234, MyApp.Cldr, unit: :year) 86 | {:ok, "in 1,234 years"} 87 | 88 | iex> Cldr.DateTime.Relative.to_string(1234, MyApp.Cldr, unit: :year, locale: "fr") 89 | {:ok, "dans 1 234 ans"} 90 | 91 | iex> Cldr.DateTime.Relative.to_string(31, MyApp.Cldr) 92 | {:ok, "in 31 seconds"} 93 | 94 | iex> Cldr.DateTime.Relative.to_string(~D[2017-04-29], MyApp.Cldr, relative_to: ~D[2017-04-26]) 95 | {:ok, "in 3 days"} 96 | 97 | iex> Cldr.DateTime.Relative.to_string(310, MyApp.Cldr, format: :short, locale: "fr") 98 | {:ok, "dans 5 min"} 99 | 100 | iex> Cldr.DateTime.Relative.to_string(310, MyApp.Cldr, format: :narrow, locale: "fr") 101 | {:ok, "+5 min"} 102 | 103 | iex> Cldr.DateTime.Relative.to_string 2, MyApp.Cldr, unit: :wed, format: :short, locale: "en" 104 | {:ok, "in 2 Wed."} 105 | 106 | iex> Cldr.DateTime.Relative.to_string 1, MyApp.Cldr, unit: :wed, format: :short 107 | {:ok, "next Wed."} 108 | 109 | iex> Cldr.DateTime.Relative.to_string -1, MyApp.Cldr, unit: :wed, format: :short 110 | {:ok, "last Wed."} 111 | 112 | iex> Cldr.DateTime.Relative.to_string -1, MyApp.Cldr, unit: :wed 113 | {:ok, "last Wednesday"} 114 | 115 | iex> Cldr.DateTime.Relative.to_string -1, MyApp.Cldr, unit: :quarter 116 | {:ok, "last quarter"} 117 | 118 | iex> Cldr.DateTime.Relative.to_string -1, MyApp.Cldr, unit: :mon, locale: "fr" 119 | {:ok, "lundi dernier"} 120 | 121 | iex> Cldr.DateTime.Relative.to_string(~D[2017-04-29], MyApp.Cldr, unit: :ziggeraut) 122 | {:error, {Cldr.DateTime.UnknownTimeUnit, 123 | "Unknown time unit :ziggeraut. Valid time units are [:day, :fri, :hour, :minute, :mon, :month, :quarter, :sat, :second, :sun, :thu, :tue, :wed, :week, :year]"}} 124 | 125 | """ 126 | 127 | @spec to_string(integer | float | Date.t() | DateTime.t(), Cldr.backend(), Keyword.t()) :: 128 | {:ok, String.t()} | {:error, {module, String.t()}} 129 | 130 | def to_string(relative, backend \\ Cldr.Date.default_backend(), options \\ []) 131 | 132 | def to_string(relative, options, []) when is_list(options) do 133 | to_string(relative, Cldr.Date.default_backend(), options) 134 | end 135 | 136 | def to_string(relative, backend, options) do 137 | options = normalize_options(backend, options) 138 | locale = Keyword.get(options, :locale) 139 | {unit, options} = Keyword.pop(options, :unit) 140 | 141 | with {:ok, locale} <- Cldr.validate_locale(locale, backend), 142 | {:ok, unit} <- validate_unit(unit), 143 | {:ok, _style} <- validate_style(options[:style] || options[:format]) do 144 | {relative, unit} = define_unit_and_relative_time(relative, unit, options[:relative_to]) 145 | string = to_string(relative, unit, locale, backend, options) 146 | {:ok, string} 147 | end 148 | end 149 | 150 | defp normalize_options(backend, options) do 151 | {locale, _backend} = Cldr.locale_and_backend_from(options[:locale], backend) 152 | style = options[:style] || options[:format] || :default 153 | 154 | options 155 | |> Keyword.put(:locale, locale) 156 | |> Keyword.put(:style, style) 157 | |> Keyword.delete(:format) 158 | end 159 | 160 | # No unit or relative_to is specified so we derive them 161 | defp define_unit_and_relative_time(relative, nil, nil) when is_number(relative) do 162 | unit = unit_from_relative_time(relative) 163 | relative = scale_relative(relative, unit) 164 | {relative, unit} 165 | end 166 | 167 | # Take two datetimes and calculate the seconds between them 168 | defp define_unit_and_relative_time( 169 | %{year: _, month: _, day: _, hour: _, minute: _, second: _, calendar: Calendar.ISO} = 170 | relative, 171 | unit, 172 | relative_to 173 | ) do 174 | now = (relative_to || DateTime.utc_now()) |> DateTime.to_unix() 175 | then = DateTime.to_unix(relative) 176 | relative_time = then - now 177 | unit = unit || unit_from_relative_time(relative_time) 178 | 179 | relative = scale_relative(relative_time, unit) 180 | {relative, unit} 181 | end 182 | 183 | # Take two dates and calculate the days between them 184 | defp define_unit_and_relative_time( 185 | %{year: _, month: _, day: _, calendar: Calendar.ISO} = relative, 186 | unit, 187 | relative_to 188 | ) do 189 | today = 190 | (relative_to || Date.utc_today()) 191 | |> Date.to_erl() 192 | |> :calendar.date_to_gregorian_days() 193 | |> Kernel.*(@day) 194 | 195 | then = 196 | relative 197 | |> Date.to_erl() 198 | |> :calendar.date_to_gregorian_days() 199 | |> Kernel.*(@day) 200 | 201 | relative_time = 202 | then - today 203 | 204 | unit = 205 | unit || unit_from_relative_time(relative_time) 206 | 207 | relative = scale_relative(relative_time, unit) 208 | {relative, unit} 209 | end 210 | 211 | # Anything else just return the values 212 | defp define_unit_and_relative_time(relative_time, unit, _relative_to) do 213 | {relative_time, unit} 214 | end 215 | 216 | @doc """ 217 | Returns a string representing a relative time (ago, in) for a given 218 | number, Date or Datetime or raises an exception on error. 219 | 220 | ## Arguments 221 | 222 | * `relative` is a number or Date/Datetime representing the time distance from `now` or from 223 | options[:relative_to]. 224 | 225 | * `backend` is any module that includes `use Cldr` and therefore 226 | is a `Cldr` backend module. The default is `Cldr.default_backend/0`. 227 | 228 | * `options` is a `Keyword` list of options. 229 | 230 | ## Options 231 | 232 | * `:locale` is the locale in which the binary is formatted. 233 | The default is `Cldr.get_locale/0`. 234 | 235 | * `:format` is the format of the binary. Format may be `:default`, `:narrow` or `:short`. 236 | The default is `:default`. 237 | 238 | * `:unit` is the time unit for the formatting. The allowable units are `:second`, `:minute`, 239 | `:hour`, `:day`, `:week`, `:month`, `:year`, `:mon`, `:tue`, `:wed`, `:thu`, `:fri`, `:sat`, 240 | `:sun`, `:quarter`. 241 | 242 | * `:relative_to` is the baseline `t:Date/0` or `t:Datetime.t/0` from which the difference 243 | from `relative` is calculated when `relative` is a Date or a DateTime. The default for 244 | a `t:Date.t/0` is `Date.utc_today/0`, for a `t:DateTime.t/0` it is `DateTime.utc_now/0`. 245 | 246 | See `to_string/3` 247 | 248 | """ 249 | @spec to_string!(integer | float | Date.t() | DateTime.t(), Cldr.backend(), Keyword.t()) :: 250 | String.t() 251 | def to_string!(relative, backend \\ Cldr.Date.default_backend(), options \\ []) 252 | 253 | def to_string!(relative, options, []) when is_list(options) do 254 | to_string!(relative, Cldr.Date.default_backend(), options) 255 | end 256 | 257 | def to_string!(relative, backend, options) do 258 | case to_string(relative, backend, options) do 259 | {:ok, string} -> string 260 | {:error, {exception, reason}} -> raise exception, reason 261 | end 262 | end 263 | 264 | @spec to_string(integer | float, atom(), Cldr.LanguageTag.t(), Cldr.backend(), Keyword.t()) :: 265 | String.t() 266 | 267 | defp to_string(relative, unit, locale, backend, options) 268 | 269 | # For the case when its relative by one unit, for example "tomorrow" or "yesterday" 270 | # or "last" 271 | defp to_string(relative, unit, locale, backend, options) when relative in -1..1 do 272 | style = options[:style] || options[:format] 273 | 274 | result = 275 | locale 276 | |> get_locale(backend) 277 | |> get_in([unit, style, :relative_ordinal]) 278 | |> Enum.at(relative + 1) 279 | 280 | if is_nil(result), do: to_string(relative / 1, unit, locale, backend, options), else: result 281 | end 282 | 283 | # For the case when its more than one unit away. For example, "in 3 days" 284 | # or "2 days ago" 285 | defp to_string(relative, unit, locale, backend, options) 286 | when is_float(relative) or is_integer(relative) do 287 | direction = if relative > 0, do: :relative_future, else: :relative_past 288 | style = options[:style] || options[:format] 289 | 290 | rules = 291 | locale 292 | |> get_locale(backend) 293 | |> get_in([unit, style, direction]) 294 | 295 | rule = Module.concat(backend, Number.Cardinal).pluralize(trunc(relative), locale, rules) 296 | 297 | relative 298 | |> abs() 299 | |> Cldr.Number.to_string!(backend, locale: locale) 300 | |> Cldr.Substitution.substitute(rule) 301 | |> Enum.join() 302 | end 303 | 304 | defp time_unit_error(unit) do 305 | {Cldr.DateTime.UnknownTimeUnit, 306 | "Unknown time unit #{inspect(unit)}. Valid time units are #{inspect(@unit_keys)}"} 307 | end 308 | 309 | defp style_error(style) do 310 | {Cldr.UnknownStyleError, 311 | "Unknown style #{inspect(style)}. Valid styles are #{inspect(@known_styles)}"} 312 | end 313 | 314 | @doc """ 315 | Returns an estimate of the appropriate time unit for an integer of a given 316 | magnitude of seconds. 317 | 318 | ## Examples 319 | 320 | iex> Cldr.DateTime.Relative.unit_from_relative_time(1234) 321 | :minute 322 | 323 | iex> Cldr.DateTime.Relative.unit_from_relative_time(12345) 324 | :hour 325 | 326 | iex> Cldr.DateTime.Relative.unit_from_relative_time(123456) 327 | :day 328 | 329 | iex> Cldr.DateTime.Relative.unit_from_relative_time(1234567) 330 | :week 331 | 332 | iex> Cldr.DateTime.Relative.unit_from_relative_time(12345678) 333 | :month 334 | 335 | iex> Cldr.DateTime.Relative.unit_from_relative_time(123456789) 336 | :year 337 | 338 | """ 339 | def unit_from_relative_time(time) when is_number(time) do 340 | case abs(time) do 341 | i when i < @minute -> :second 342 | i when i < @hour -> :minute 343 | i when i < @day -> :hour 344 | i when i < @week -> :day 345 | i when i < @month -> :week 346 | i when i < @year -> :month 347 | _ -> :year 348 | end 349 | end 350 | 351 | def unit_from_relative_time(time) do 352 | time 353 | end 354 | 355 | @doc """ 356 | Calculates the time span in the given `unit` from the time given in seconds. 357 | 358 | ## Examples 359 | 360 | iex> Cldr.DateTime.Relative.scale_relative(1234, :second) 361 | 1234 362 | 363 | iex> Cldr.DateTime.Relative.scale_relative(1234, :minute) 364 | 21 365 | 366 | iex> Cldr.DateTime.Relative.scale_relative(1234, :hour) 367 | 0 368 | 369 | """ 370 | def scale_relative(time, unit) when is_number(time) and is_atom(unit) do 371 | (time / @unit[unit]) 372 | |> Float.round() 373 | |> trunc 374 | end 375 | 376 | @doc """ 377 | Returns a list of the valid unit keys for `to_string/2` 378 | 379 | ## Example 380 | 381 | iex> Cldr.DateTime.Relative.known_units() 382 | [:day, :fri, :hour, :minute, :mon, :month, :quarter, :sat, :second, 383 | :sun, :thu, :tue, :wed, :week, :year] 384 | 385 | """ 386 | def known_units do 387 | @unit_keys 388 | end 389 | 390 | defp validate_unit(unit) when unit in @unit_keys or is_nil(unit) do 391 | {:ok, unit} 392 | end 393 | 394 | defp validate_unit(unit) do 395 | {:error, time_unit_error(unit)} 396 | end 397 | 398 | def known_styles do 399 | @known_styles 400 | end 401 | 402 | defp validate_style(style) when style in @known_styles do 403 | {:ok, style} 404 | end 405 | 406 | defp validate_style(style) do 407 | {:error, style_error(style)} 408 | end 409 | 410 | defp get_locale(locale, backend) do 411 | backend = Module.concat(backend, DateTime.Relative) 412 | backend.get_locale(locale) 413 | end 414 | end 415 | -------------------------------------------------------------------------------- /lib/cldr/format/compiler.ex: -------------------------------------------------------------------------------- 1 | defmodule Cldr.DateTime.Format.Compiler do 2 | @moduledoc """ 3 | Tokenizes and parses `Date`, `Time` and `DateTime` format strings. 4 | 5 | During compilation, each of the date, time and datetime format 6 | strings defined in CLDR are compiled into a list of 7 | function bodies that are then grafted onto the function head 8 | `format/3` in a backend module. As a result these compiled 9 | formats execute with good performance. 10 | 11 | For formats not defined in CLDR (ie a user defined format), 12 | the tokenizing and parsing is performed, then list of function 13 | bodies is created and then `format/3` 14 | recurses over the list, invoking each function and 15 | collecting the results. This process is significantly slower 16 | than that of the precompiled formats. 17 | 18 | User defined formats can also be precompiled by configuring 19 | them under the key `:precompile_datetime_formats`. For example: 20 | 21 | config :ex_cldr, 22 | precompile_datetime_formats: ["yy/dd", "hhh:mmm:sss"] 23 | 24 | """ 25 | 26 | @doc """ 27 | Tokenize a date, time or datetime format string. 28 | 29 | This function is designed to produce output 30 | that is fed into `Cldr.DateTime.Format.Compiler.compile/3`. 31 | 32 | ## Arguments 33 | 34 | * `format_string` is a date, datetime or time format 35 | string. 36 | 37 | ## Returns 38 | 39 | A list of 3-tuples which represent the tokens 40 | of the format definition. 41 | 42 | ## Example 43 | 44 | iex> Cldr.DateTime.Format.Compiler.tokenize("yyyy/MM/dd") 45 | {:ok, 46 | [{:year, 1, 4}, {:literal, 1, "/"}, {:month, 1, 2}, {:literal, 1, "/"}, 47 | {:day_of_month, 1, 2}], 1} 48 | 49 | """ 50 | def tokenize(format_string) when is_binary(format_string) do 51 | format_string 52 | |> String.to_charlist() 53 | |> :date_time_format_lexer.string() 54 | end 55 | 56 | def tokenize(%{number_system: _numbers, format: format_string}) do 57 | tokenize(format_string) 58 | end 59 | 60 | @doc """ 61 | Parse a date, time or datetime format string. 62 | 63 | ## Arguments 64 | 65 | * `format_string` is a string defining how a date/time/datetime 66 | is to be formatted. See `Cldr.DateTime.Formatter` for the list 67 | of supported format symbols. 68 | 69 | ## Returns 70 | 71 | Returns a list of function bodies which are grafted onto 72 | a function head in `Cldr.DateTime.Formatter` at compile time 73 | to produce a series of functions that process a given format 74 | string efficiently. 75 | 76 | """ 77 | @spec compile(String.t(), module(), module()) :: 78 | {:ok, Macro.t()} | {:error, String.t()} 79 | 80 | def compile(format_string, backend, context) 81 | 82 | def compile("", _, _) do 83 | {:error, "empty format string cannot be compiled"} 84 | end 85 | 86 | def compile(nil, _, _) do 87 | {:error, "no format string or token list provided"} 88 | end 89 | 90 | def compile(definition, backend, context) when is_binary(definition) do 91 | with {:ok, tokens, _end_line} <- tokenize(definition) do 92 | transforms = 93 | Enum.map(tokens, fn {fun, _line, count} -> 94 | quote do 95 | Cldr.DateTime.Formatter.unquote(fun)( 96 | var!(date, unquote(context)), 97 | unquote(count), 98 | var!(locale, unquote(context)), 99 | unquote(backend), 100 | var!(options, unquote(context)) 101 | ) 102 | end 103 | end) 104 | 105 | {:ok, transforms} 106 | else 107 | error -> 108 | raise ArgumentError, "Could not parse #{inspect(definition)}: #{inspect(error)}" 109 | end 110 | end 111 | 112 | def compile(%{number_system: _number_system, format: value}, backend, context) do 113 | compile(value, backend, context) 114 | end 115 | 116 | def compile(arg, _, _) do 117 | raise ArgumentError, message: "No idea how to compile format: #{inspect(arg)}" 118 | end 119 | 120 | @doc false 121 | def tokenize_skeleton(token_id) when is_atom(token_id) do 122 | token_id 123 | |> Atom.to_string() 124 | |> tokenize_skeleton() 125 | end 126 | 127 | def tokenize_skeleton(token_id) when is_binary(token_id) do 128 | tokenized = 129 | token_id 130 | |> String.to_charlist() 131 | |> :skeleton_tokenizer.string() 132 | 133 | case tokenized do 134 | {:ok, tokens, _} -> 135 | {:ok, tokens} 136 | 137 | {:error, {_, :skeleton_tokenizer, {:illegal, content}}, _} -> 138 | {:error, "Illegal format string content found at: #{inspect(content)}"} 139 | end 140 | end 141 | end 142 | -------------------------------------------------------------------------------- /lib/cldr/format/date_time_timezone.ex: -------------------------------------------------------------------------------- 1 | defmodule Cldr.DateTime.Timezone do 2 | @moduledoc false 3 | 4 | @doc """ 5 | Converts the time zone offset of a `Time` or `DateTime` into 6 | seconds. 7 | """ 8 | def time_from_zone_offset(%{utc_offset: utc_offset, std_offset: std_offset}) do 9 | offset = utc_offset + std_offset 10 | 11 | hours = div(offset, 3600) 12 | minutes = div(offset - hours * 3600, 60) 13 | seconds = offset - hours * 3600 - minutes * 60 14 | {hours, minutes, seconds} 15 | end 16 | 17 | def time_from_zone_offset(other) do 18 | Cldr.DateTime.Formatter.error_return(other, "x", [:utc_offset]) 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/cldr/interval/time.ex: -------------------------------------------------------------------------------- 1 | defmodule Cldr.Time.Interval do 2 | @moduledoc """ 3 | Interval formats allow for software to format intervals like "Jan 10-12, 2008" as a 4 | shorter and more natural format than "Jan 10, 2008 - Jan 12, 2008". They are designed 5 | to take a start and end date, time or datetime plus a formatting pattern 6 | and use that information to produce a localized format. 7 | 8 | See `Cldr.Interval.to_string/3` and `Cldr.Time.Interval.to_string/3` 9 | 10 | """ 11 | 12 | alias Cldr.DateTime.Format 13 | import Cldr.DateTime, only: [apply_preference: 2] 14 | 15 | import Cldr.Date.Interval, 16 | only: [ 17 | format_error: 2, 18 | style_error: 1 19 | ] 20 | 21 | import Cldr.Calendar, 22 | only: [ 23 | time: 0 24 | ] 25 | 26 | # Time styles not defined 27 | # by a grouping but can still 28 | # be used directly 29 | 30 | @doc false 31 | @style_map %{ 32 | # Can be used with any time 33 | time: %{ 34 | h12: %{ 35 | short: :h, 36 | medium: :hm, 37 | long: :hm 38 | }, 39 | h23: %{ 40 | short: :H, 41 | medium: :Hm, 42 | long: :Hm 43 | } 44 | }, 45 | 46 | # Includes the timezone 47 | zone: %{ 48 | h12: %{ 49 | short: :hv, 50 | medium: :hmv, 51 | long: :hmv 52 | }, 53 | h23: %{ 54 | short: :Hv, 55 | medium: :Hmv, 56 | long: :Hmv 57 | } 58 | }, 59 | 60 | # Includes flex times 61 | # annotation like 62 | # ".. in the evening" 63 | flex: %{ 64 | h12: %{ 65 | short: :Bh, 66 | medium: :Bhm, 67 | long: :Bhm 68 | }, 69 | h23: %{ 70 | short: :Bh, 71 | medium: :Bhm, 72 | long: :Bhm 73 | } 74 | } 75 | } 76 | 77 | @styles Map.keys(@style_map) 78 | @formats Map.keys(@style_map.time.h12) 79 | 80 | @default_format :medium 81 | @default_style :time 82 | @default_prefer :default 83 | 84 | def styles do 85 | @style_map 86 | end 87 | 88 | @doc false 89 | def to_string(unquote(time()) = from, unquote(time()) = to) do 90 | {locale, backend} = Cldr.locale_and_backend_from(nil, nil) 91 | to_string(from, to, backend, locale: locale) 92 | end 93 | 94 | def to_string(nil = from, unquote(time()) = to) do 95 | {locale, backend} = Cldr.locale_and_backend_from(nil, nil) 96 | to_string(from, to, backend, locale: locale) 97 | end 98 | 99 | def to_string(unquote(time()) = from, nil = to) do 100 | {locale, backend} = Cldr.locale_and_backend_from(nil, nil) 101 | to_string(from, to, backend, locale: locale) 102 | end 103 | 104 | @doc false 105 | def to_string(unquote(time()) = from, unquote(time()) = to, backend) when is_atom(backend) do 106 | {locale, backend} = Cldr.locale_and_backend_from(nil, backend) 107 | to_string(from, to, backend, locale: locale) 108 | end 109 | 110 | def to_string(nil = from, unquote(time()) = to, backend) when is_atom(backend) do 111 | {locale, backend} = Cldr.locale_and_backend_from(nil, backend) 112 | to_string(from, to, backend, locale: locale) 113 | end 114 | 115 | def to_string(unquote(time()) = from, nil = to, backend) when is_atom(backend) do 116 | {locale, backend} = Cldr.locale_and_backend_from(nil, backend) 117 | to_string(from, to, backend, locale: locale) 118 | end 119 | 120 | @doc false 121 | def to_string(unquote(time()) = from, unquote(time()) = to, options) when is_list(options) do 122 | {locale, backend} = Cldr.locale_and_backend_from(options) 123 | to_string(from, to, backend, Keyword.put_new(options, :locale, locale)) 124 | end 125 | 126 | def to_string(nil = from, unquote(time()) = to, options) when is_list(options) do 127 | {locale, backend} = Cldr.locale_and_backend_from(options) 128 | to_string(from, to, backend, Keyword.put_new(options, :locale, locale)) 129 | end 130 | 131 | def to_string(unquote(time()) = from, nil = to, options) when is_list(options) do 132 | {locale, backend} = Cldr.locale_and_backend_from(options) 133 | to_string(from, to, backend, Keyword.put_new(options, :locale, locale)) 134 | end 135 | 136 | @doc """ 137 | Returns a string representing the formatted 138 | interval formed by two times. 139 | 140 | ### Arguments 141 | 142 | * `from` is any map that conforms to the 143 | `Calendar.time` type. 144 | 145 | * `to` is any map that conforms to the 146 | `Calendar.time` type. `to` must occur 147 | on or after `from`. 148 | 149 | * `backend` is any module that includes `use Cldr` and 150 | is therefore `Cldr` backend module 151 | 152 | * `options` is a keyword list of options. The default is 153 | `[format: :medium, style: :time]`. 154 | 155 | Either `from` or `to` may also be `nil` in which case the 156 | interval is formatted as an open interval with the non-nil 157 | side formatted as a standalone time. 158 | 159 | ### Options 160 | 161 | * `:format` is one of `:short`, `:medium` or `:long` or a 162 | specific format type or a string representing of an interval 163 | format. The default is `:medium`. 164 | 165 | * `:style` supports different formatting styles. The 166 | alternatives are `:time`, `:zone`, 167 | and `:flex`. The default is `:time`. 168 | 169 | * `:locale` is any valid locale name returned by `Cldr.known_locale_names/0` 170 | or a `t:Cldr.LanguageTag.t/0` struct. The default is `Cldr.get_locale/0`. 171 | 172 | * `:number_system` a number system into which the formatted date digits should 173 | be transliterated. 174 | 175 | * `:prefer` expresses the preference for one of the possible alternative 176 | sub-formats. See the variant preference notes below. 177 | 178 | ### Variant Preference 179 | 180 | * A small number of formats have one of two different alternatives, each with their own 181 | preference specifier. The preferences are specified with the `:prefer` option to 182 | `Cldr.Date.to_string/3`. The preference is expressed as an atom, or a list of one or two 183 | atoms with one atom being either `:unicode` or `:ascii` and one atom being either 184 | `:default` or `:variant`. 185 | 186 | * Some formats (at the time of publishng only time formats but that 187 | may change in the future) have `:unicode` and `:ascii` versions of the format. The 188 | difference is the use of ascii space (0x20) as a separateor in the `:ascii` verison 189 | whereas the `:unicode` version may use non-breaking or other space characters. The 190 | default is `:unicode` and this is the strongly preferred option. The `:ascii` format 191 | is primarily to support legacy use cases and is not recommended. See 192 | `Cldr.Date.available_formats/3` to see which formats have these variants. 193 | 194 | * Some formats (at the time of publishing, only date and datetime formats) have 195 | `:default` and `:variant` versions of the format. These variant formats are only 196 | included in a small number of locales. For example, the `:"en-CA"` locale, which has 197 | a `:default` format respecting typical Canadian formatting and a `:variant` that is 198 | more closely aligned to US formatting. The default is `:default`. 199 | 200 | ### Returns 201 | 202 | * `{:ok, string}` or 203 | 204 | * `{:error, {exception, reason}}` 205 | 206 | ## Notes 207 | 208 | * For more information on interval format string 209 | see `Cldr.Interval`. 210 | 211 | * The available predefined formats that can be applied are the 212 | keys of the map returned by `Cldr.DateTime.Format.interval_formats("en", :gregorian)` 213 | where `"en"` can be replaced by any configured locale name and `:gregorian` 214 | is the underlying CLDR calendar type. 215 | 216 | * In the case where `from` and `to` are equal, a single 217 | time is formatted instead of an interval. 218 | 219 | ### Examples 220 | 221 | iex> Cldr.Time.Interval.to_string ~T[10:00:00], ~T[10:03:00], MyApp.Cldr, format: :short 222 | {:ok, "10 – 10 AM"} 223 | 224 | iex> Cldr.Time.Interval.to_string ~T[10:00:00], ~T[10:03:00], MyApp.Cldr, format: :medium 225 | {:ok, "10:00 – 10:03 AM"} 226 | 227 | iex> Cldr.Time.Interval.to_string ~T[10:00:00], ~T[10:03:00], MyApp.Cldr, format: :long 228 | {:ok, "10:00 – 10:03 AM"} 229 | 230 | iex> Cldr.Time.Interval.to_string ~T[10:00:00], ~T[10:03:00], MyApp.Cldr, 231 | ...> format: :long, style: :flex 232 | {:ok, "10:00 – 10:03 in the morning"} 233 | 234 | iex> Cldr.Time.Interval.to_string ~U[2020-01-01 00:00:00.0Z], ~U[2020-01-01 10:00:00.0Z], 235 | ...> MyApp.Cldr, format: :long, style: :flex 236 | {:ok, "12:00 – 10:00 in the morning"} 237 | 238 | iex> Cldr.Time.Interval.to_string ~U[2020-01-01 00:00:00.0Z], nil, MyApp.Cldr, 239 | ...> format: :long, style: :flex 240 | {:ok, "12:00:00 AM UTC –"} 241 | 242 | iex> Cldr.Time.Interval.to_string ~U[2020-01-01 00:00:00.0Z], ~U[2020-01-01 10:00:00.0Z], 243 | ...> MyApp.Cldr, format: :long, style: :zone 244 | {:ok, "12:00 – 10:00 AM Etc/UTC"} 245 | 246 | iex> Cldr.Time.Interval.to_string ~T[10:00:00], ~T[10:03:00], MyApp.Cldr, 247 | ...> format: :long, style: :flex, locale: "th" 248 | {:ok, "10:00 – 10:03 ในตอนเช้า"} 249 | 250 | """ 251 | @spec to_string(Calendar.time() | nil, Calendar.time() | nil, Cldr.backend(), Keyword.t()) :: 252 | {:ok, String.t()} | {:error, {module, String.t()}} 253 | 254 | def to_string(from, to, backend, options \\ []) 255 | 256 | def to_string(%{calendar: calendar} = from, %{calendar: calendar} = to, backend, options) 257 | when calendar == Calendar.ISO do 258 | from = %{from | calendar: Cldr.Calendar.Gregorian} 259 | to = %{to | calendar: Cldr.Calendar.Gregorian} 260 | 261 | to_string(from, to, backend, options) 262 | end 263 | 264 | def to_string(unquote(time()) = from, unquote(time()) = to, backend, options) do 265 | {locale, backend} = Cldr.locale_and_backend_from(options[:locale], backend) 266 | formatter = Module.concat(backend, DateTime.Formatter) 267 | options = normalize_options(locale, backend, options) 268 | 269 | number_system = options.number_system 270 | prefer = options.prefer 271 | format = options.format 272 | 273 | with {:ok, backend} <- Cldr.validate_backend(backend), 274 | {:ok, locale} <- Cldr.validate_locale(locale, backend), 275 | {:ok, _} <- Cldr.Number.validate_number_system(locale, number_system, backend), 276 | {:ok, calendar} <- Cldr.Calendar.validate_calendar(from.calendar), 277 | {:ok, formats} <- Format.interval_formats(locale, calendar.cldr_calendar_type(), backend), 278 | {:ok, format} <- resolve_format(from, to, formats, locale, options), 279 | {:ok, [left, right]} <- apply_preference(format, prefer), 280 | {:ok, left_format} <- formatter.format(from, left, locale, options), 281 | {:ok, right_format} <- formatter.format(to, right, locale, options) do 282 | {:ok, left_format <> right_format} 283 | else 284 | {:error, :no_practical_difference} -> 285 | options = Cldr.DateTime.Interval.adjust_options(options, locale, format) 286 | Cldr.Time.to_string(from, backend, options) 287 | 288 | other -> 289 | other 290 | end 291 | end 292 | 293 | # Open ended intervals use the `date_time_interval_fallback/0` format 294 | def to_string(nil, unquote(time()) = to, backend, options) do 295 | {locale, backend} = Cldr.locale_and_backend_from(options[:locale], backend) 296 | 297 | with {:ok, formatted} <- Cldr.Time.to_string(to, backend, options) do 298 | pattern = Module.concat(backend, DateTime.Format).date_time_interval_fallback(locale) 299 | 300 | result = 301 | ["", formatted] 302 | |> Cldr.Substitution.substitute(pattern) 303 | |> Enum.join() 304 | |> String.trim_leading() 305 | 306 | {:ok, result} 307 | end 308 | end 309 | 310 | def to_string(unquote(time()) = from, nil, backend, options) do 311 | {locale, backend} = Cldr.locale_and_backend_from(options[:locale], backend) 312 | 313 | with {:ok, formatted} <- Cldr.Time.to_string(from, backend, options) do 314 | pattern = Module.concat(backend, DateTime.Format).date_time_interval_fallback(locale) 315 | 316 | result = 317 | [formatted, ""] 318 | |> Cldr.Substitution.substitute(pattern) 319 | |> Enum.join() 320 | |> String.trim_trailing() 321 | 322 | {:ok, result} 323 | end 324 | end 325 | 326 | @doc false 327 | def to_string!(unquote(time()) = from, unquote(time()) = to) do 328 | {locale, backend} = Cldr.locale_and_backend_from(nil, nil) 329 | to_string!(from, to, backend, locale: locale) 330 | end 331 | 332 | @doc """ 333 | Returns a string representing the formatted 334 | interval formed by two times. 335 | 336 | ### Arguments 337 | 338 | * `from` is any map that conforms to the 339 | `Calendar.time` type. 340 | 341 | * `to` is any map that conforms to the 342 | `Calendar.time` type. 343 | 344 | * `backend` is any module that includes `use Cldr` and 345 | is therefore `Cldr` backend module 346 | 347 | * `options` is a keyword list of options. The default is 348 | `[format: :medium, style: :time]`. 349 | 350 | ### Options 351 | 352 | * `:format` is one of `:short`, `:medium` or `:long` or a 353 | specific format type or a string representing of an interval 354 | format. The default is `:medium`. 355 | 356 | * `:style` supports different formatting styles. The 357 | alternatives are `:time`, `:zone`, 358 | and `:flex`. The default is `:time`. 359 | 360 | * `:locale` is any valid locale name returned by `Cldr.known_locale_names/0` 361 | or a `t:Cldr.LanguageTag.t/0` struct. The default is `Cldr.get_locale/0` 362 | 363 | * `:number_system` a number system into which the formatted date digits should 364 | be transliterated. 365 | 366 | * `:prefer` expresses the preference for one of the possible alternative 367 | sub-formats. See the variant preference notes below. 368 | 369 | ### Variant Preference 370 | 371 | * A small number of formats have one of two different alternatives, each with their own 372 | preference specifier. The preferences are specified with the `:prefer` option to 373 | `Cldr.Date.to_string/3`. The preference is expressed as an atom, or a list of one or two 374 | atoms with one atom being either `:unicode` or `:ascii` and one atom being either 375 | `:default` or `:variant`. 376 | 377 | * Some formats (at the time of publishng only time formats but that 378 | may change in the future) have `:unicode` and `:ascii` versions of the format. The 379 | difference is the use of ascii space (0x20) as a separateor in the `:ascii` verison 380 | whereas the `:unicode` version may use non-breaking or other space characters. The 381 | default is `:unicode` and this is the strongly preferred option. The `:ascii` format 382 | is primarily to support legacy use cases and is not recommended. See 383 | `Cldr.Date.available_formats/3` to see which formats have these variants. 384 | 385 | * Some formats (at the time of publishing, only date and datetime formats) have 386 | `:default` and `:variant` versions of the format. These variant formats are only 387 | included in a small number of locales. For example, the `:"en-CA"` locale, which has 388 | a `:default` format respecting typical Canadian formatting and a `:variant` that is 389 | more closely aligned to US formatting. The default is `:default`. 390 | 391 | ### Returns 392 | 393 | * `string` or 394 | 395 | * raises an exception 396 | 397 | ### Notes 398 | 399 | * For more information on interval format string 400 | see `Cldr.Interval`. 401 | 402 | * The available predefined formats that can be applied are the 403 | keys of the map returned by `Cldr.DateTime.Format.interval_formats("en", :gregorian)` 404 | where `"en"` can be replaced by any configured locale name and `:gregorian` 405 | is the underlying CLDR calendar type. 406 | 407 | * In the case where `from` and `to` are equal, a single 408 | time is formatted instead of an interval. 409 | 410 | ### Examples 411 | 412 | iex> Cldr.Time.Interval.to_string! ~T[10:00:00], ~T[10:03:00], MyApp.Cldr, format: :short 413 | "10 – 10 AM" 414 | 415 | iex> Cldr.Time.Interval.to_string! ~T[10:00:00], ~T[10:03:00], MyApp.Cldr, format: :medium 416 | "10:00 – 10:03 AM" 417 | 418 | iex> Cldr.Time.Interval.to_string! ~T[10:00:00], ~T[10:03:00], MyApp.Cldr, format: :long 419 | "10:00 – 10:03 AM" 420 | 421 | iex> Cldr.Time.Interval.to_string ~T[23:00:00.0Z], ~T[01:01:00.0Z], MyApp.Cldr 422 | {:ok, "11:00 PM – 1:01 AM"} 423 | 424 | iex> Cldr.Time.Interval.to_string! ~T[10:00:00], ~T[10:03:00], MyApp.Cldr, 425 | ...> format: :long, style: :flex 426 | "10:00 – 10:03 in the morning" 427 | 428 | iex> Cldr.Time.Interval.to_string! ~U[2020-01-01 00:00:00.0Z], ~U[2020-01-01 10:00:00.0Z], 429 | ...> MyApp.Cldr, format: :long, style: :flex 430 | "12:00 – 10:00 in the morning" 431 | 432 | iex> Cldr.Time.Interval.to_string! ~U[2020-01-01 00:00:00.0Z], ~U[2020-01-01 10:00:00.0Z], 433 | ...> MyApp.Cldr, format: :long, style: :zone 434 | "12:00 – 10:00 AM Etc/UTC" 435 | 436 | iex> Cldr.Time.Interval.to_string! ~T[10:00:00], ~T[10:03:00], MyApp.Cldr, 437 | ...> format: :long, style: :flex, locale: "th" 438 | "10:00 – 10:03 ในตอนเช้า" 439 | 440 | """ 441 | def to_string!(from, to, backend, options \\ []) do 442 | case to_string(from, to, backend, options) do 443 | {:ok, string} -> string 444 | {:error, {exception, reason}} -> raise exception, reason 445 | end 446 | end 447 | 448 | defp normalize_options(_locale, _backend, %{} = options) do 449 | options 450 | end 451 | 452 | defp normalize_options(locale, backend, options) do 453 | format = options[:time_format] || options[:format] || @default_format 454 | locale_number_system = Cldr.Number.System.number_system_from_locale(locale, backend) 455 | number_system = Keyword.get(options, :number_system, locale_number_system) 456 | prefer = Keyword.get(options, :prefer, @default_prefer) 457 | style = Keyword.get(options, :style, @default_style) 458 | 459 | options 460 | |> Map.new() 461 | |> Map.put(:format, format) 462 | |> Map.put(:style, style) 463 | |> Map.put(:locale, locale) 464 | |> Map.put(:number_system, number_system) 465 | |> Map.put(:prefer, prefer) 466 | end 467 | 468 | @doc """ 469 | Returns the format code representing the date or 470 | time unit that is the greatest difference between 471 | two times. 472 | 473 | Only differences in hours or minutes are considered. 474 | 475 | ### Arguments 476 | 477 | * `from` is any `t:Time.t/0`. 478 | 479 | * `to` is any `t:Time.t/0`. 480 | 481 | ### Returns 482 | 483 | * `{:ok, format_code}` where `format_code` is one of: 484 | 485 | * `:H` meaning that the greatest difference is in the hour 486 | * `:m` meaning that the greatest difference is in the minute 487 | 488 | * `{:error, :no_practical_difference}` 489 | 490 | ### Example 491 | 492 | iex> Cldr.Time.Interval.greatest_difference ~T[10:11:00], ~T[10:12:00] 493 | {:ok, :m} 494 | 495 | iex> Cldr.Time.Interval.greatest_difference ~T[10:11:00], ~T[10:11:00] 496 | {:error, :no_practical_difference} 497 | 498 | """ 499 | def greatest_difference(from, to) do 500 | Cldr.Date.Interval.greatest_difference(from, to) 501 | end 502 | 503 | defp resolve_format(from, to, formats, locale, options) do 504 | with {:ok, style} <- validate_style(options.style), 505 | {:ok, format} <- validate_format(formats, style, locale, options.format), 506 | {:ok, greatest_difference} <- greatest_difference(from, to) do 507 | greatest_difference_format(from, to, format, greatest_difference) 508 | end 509 | end 510 | 511 | defp greatest_difference_format(_from, _to, format, _) when is_list(format) do 512 | {:ok, format} 513 | end 514 | 515 | defp greatest_difference_format(%{hour: from}, %{hour: to}, format, :H) 516 | when (from < 12 and to >= 12) or (from >= 12 and to < 12) do 517 | case Map.get(format, :b) || Map.get(format, :a) || Map.get(format, :H) || Map.get(format, :h) do 518 | nil -> {:error, format_error(format, format)} 519 | success -> {:ok, success} 520 | end 521 | end 522 | 523 | defp greatest_difference_format(_from, _to, format, :H) do 524 | case Map.get(format, :h) || Map.get(format, :H) do 525 | nil -> {:error, format_error(format, format)} 526 | success -> {:ok, success} 527 | end 528 | end 529 | 530 | defp greatest_difference_format(from, to, format, :m = difference) do 531 | case Map.get(format, difference) do 532 | nil -> greatest_difference_format(from, to, format, :H) 533 | success -> {:ok, success} 534 | end 535 | end 536 | 537 | defp greatest_difference_format(_from, _to, _format, _difference) do 538 | {:error, :no_practical_difference} 539 | end 540 | 541 | defp validate_style(style) when style in @styles, do: {:ok, style} 542 | defp validate_style(style), do: {:error, style_error(style)} 543 | 544 | # Using standard format terms like :short, :medium, :long 545 | defp validate_format(formats, style, locale, format) when format in @formats do 546 | hour_format = Cldr.Time.hour_format_from_locale(locale) 547 | 548 | format_key = 549 | styles() 550 | |> Map.fetch!(style) 551 | |> Map.fetch!(hour_format) 552 | |> Map.fetch!(format) 553 | 554 | Map.fetch(formats, format_key) 555 | end 556 | 557 | # Direct specification of a format 558 | defp validate_format(formats, _style, _locale, format_key) when is_atom(format_key) do 559 | case Map.fetch(formats, format_key) do 560 | :error -> {:error, format_error(formats, format_key)} 561 | success -> success 562 | end 563 | end 564 | 565 | # Direct specification of a format as a string 566 | defp validate_format(_formats, _style, _locale, format) when is_binary(format) do 567 | Cldr.DateTime.Format.split_interval(format) 568 | end 569 | end 570 | -------------------------------------------------------------------------------- /lib/cldr/protocol/cldr_chars.ex: -------------------------------------------------------------------------------- 1 | defimpl Cldr.Chars, for: Date do 2 | def to_string(date) do 3 | locale = Cldr.get_locale() 4 | Cldr.Date.to_string!(date, locale.backend, locale: locale) 5 | end 6 | end 7 | 8 | defimpl Cldr.Chars, for: Time do 9 | def to_string(date) do 10 | locale = Cldr.get_locale() 11 | Cldr.Time.to_string!(date, locale.backend, locale: locale) 12 | end 13 | end 14 | 15 | defimpl Cldr.Chars, for: DateTime do 16 | def to_string(datetime) do 17 | locale = Cldr.get_locale() 18 | Cldr.DateTime.to_string!(datetime, locale.backend, locale: locale) 19 | end 20 | end 21 | 22 | defimpl Cldr.Chars, for: NaiveDateTime do 23 | def to_string(datetime) do 24 | locale = Cldr.get_locale() 25 | Cldr.DateTime.to_string!(datetime, locale.backend, locale: locale) 26 | end 27 | end 28 | 29 | defimpl Cldr.Chars, for: Date.Range do 30 | def to_string(range) do 31 | locale = Cldr.get_locale() 32 | Cldr.Date.Interval.to_string!(range, locale.backend, locale: locale) 33 | end 34 | end 35 | 36 | if Cldr.Code.ensure_compiled?(CalendarInterval) do 37 | defimpl Cldr.Chars, for: CalendarInterval do 38 | def to_string(interval) do 39 | locale = Cldr.get_locale() 40 | Cldr.DateTime.Interval.to_string!(interval, locale.backend, locale: locale) 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elixir-cldr/cldr_dates_times/a7df9b8103d4f9edd8cb731552396f2efc6ed112/logo.png -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Cldr.DatesTimes.Mixfile do 2 | use Mix.Project 3 | 4 | @version "2.22.0" 5 | 6 | def project do 7 | [ 8 | app: :ex_cldr_dates_times, 9 | version: @version, 10 | name: "Cldr Dates & Times", 11 | source_url: "https://github.com/elixir-cldr/cldr_dates_times", 12 | docs: docs(), 13 | elixir: "~> 1.12", 14 | description: description(), 15 | package: package(), 16 | start_permanent: Mix.env() == :prod, 17 | deps: deps(), 18 | compilers: [:leex, :yecc] ++ Mix.compilers(), 19 | elixirc_paths: elixirc_paths(Mix.env()), 20 | dialyzer: [ 21 | ignore_warnings: ".dialyzer_ignore_warnings", 22 | plt_add_apps: ~w(calendar_interval)a, 23 | flags: [ 24 | :error_handling, 25 | :unknown, 26 | :underspecs, 27 | :extra_return, 28 | :missing_return 29 | ] 30 | ], 31 | xref: [exclude: [:eprof]] 32 | ] 33 | end 34 | 35 | defp description do 36 | """ 37 | Date, Time and DateTime localization, internationalization and formatting 38 | functions using the Common Locale Data Repository (CLDR). 39 | """ 40 | end 41 | 42 | def application do 43 | [ 44 | extra_applications: [:logger, :tools] 45 | ] 46 | end 47 | 48 | def docs do 49 | [ 50 | source_ref: "v#{@version}", 51 | main: "readme", 52 | extras: ["README.md", "CHANGELOG.md", "LICENSE.md"], 53 | logo: "logo.png", 54 | formatters: ["html"], 55 | groups_for_modules: groups_for_modules(), 56 | skip_undefined_reference_warnings_on: ["changelog", "readme", "CHANGELOG.md"] 57 | ] 58 | end 59 | 60 | defp groups_for_modules do 61 | [ 62 | Interval: [ 63 | Cldr.Interval, 64 | Cldr.Date.Interval, 65 | Cldr.Time.Interval, 66 | Cldr.DateTime.Interval 67 | ], 68 | Helpers: [ 69 | Cldr.DateTime.Format.Compiler, 70 | Cldr.DateTime.Format, 71 | Cldr.DateTime.Formatter, 72 | Cldr.DateTime.Timezone 73 | ] 74 | ] 75 | end 76 | 77 | defp deps do 78 | [ 79 | {:ex_cldr_numbers, "~> 2.34"}, 80 | {:ex_cldr_calendars, "~> 2.1"}, 81 | {:ex_cldr_units, "~> 3.18", optional: true}, 82 | 83 | {:calendar_interval, "~> 0.2", optional: true}, 84 | {:ex_doc, "~> 0.25", optional: true, only: [:dev, :release], runtime: false}, 85 | {:jason, "~> 1.0", optional: true}, 86 | {:tz, "~> 0.26", optional: true}, 87 | {:benchee, "~> 1.0", optional: true, only: :dev, runtime: false}, 88 | {:dialyxir, "~> 1.0", optional: true, only: [:dev, :test], runtime: false}, 89 | {:exprof, "~> 0.2", optional: true, only: :dev, runtime: false} 90 | ] 91 | end 92 | 93 | defp package do 94 | [ 95 | maintainers: ["Kip Cole"], 96 | licenses: ["Apache-2.0"], 97 | links: links(), 98 | files: [ 99 | "lib", 100 | "src/date_time_format_lexer.xrl", 101 | "src/skeleton_tokenizer.xrl", 102 | "config", 103 | "mix.exs", 104 | "README*", 105 | "CHANGELOG*", 106 | "LICENSE*" 107 | ] 108 | ] 109 | end 110 | 111 | def links do 112 | %{ 113 | "GitHub" => "https://github.com/elixir-cldr/cldr_dates_times", 114 | "Changelog" => 115 | "https://github.com/elixir-cldr/cldr_dates_times/blob/v#{@version}/CHANGELOG.md", 116 | "Readme" => "https://github.com/elixir-cldr/cldr_dates_times/blob/v#{@version}/README.md" 117 | } 118 | end 119 | 120 | defp elixirc_paths(:test), do: ["lib", "mix", "test"] 121 | defp elixirc_paths(:dev), do: ["lib", "mix"] 122 | defp elixirc_paths(:docs), do: ["lib", "mix"] 123 | defp elixirc_paths(_), do: ["lib"] 124 | end 125 | -------------------------------------------------------------------------------- /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 | "calendar_interval": {:hex, :calendar_interval, "0.2.0", "2b253b1e37ee1d4344639a3cbfb12abd0e996e4a8181537eb33c3e93fdfaffd9", [:mix], [], "hexpm", "c13d5e0108e61808a38f622987e1c5e881d96d28945213d3efe6dd06c28ba7b0"}, 4 | "cldr_utils": {:hex, :cldr_utils, "2.28.2", "f500667164a9043369071e4f9dcef31f88b8589b2e2c07a1eb9f9fa53cb1dce9", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.5", [hex: :certifi, repo: "hexpm", optional: true]}, {:decimal, "~> 1.9 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "c506eb1a170ba7cdca59b304ba02a56795ed119856662f6b1a420af80ec42551"}, 5 | "coerce": {:hex, :coerce, "1.0.1", "211c27386315dc2894ac11bc1f413a0e38505d808153367bd5c6e75a4003d096", [:mix], [], "hexpm", "b44a691700f7a1a15b4b7e2ff1fa30bebd669929ac8aa43cffe9e2f8bf051cf1"}, 6 | "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, 7 | "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"}, 8 | "dialyxir": {:hex, :dialyxir, "1.4.5", "ca1571ac18e0f88d4ab245f0b60fa31ff1b12cbae2b11bd25d207f865e8ae78a", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b0fb08bb8107c750db5c0b324fa2df5ceaa0f9307690ee3c1f6ba5b9eb5d35c3"}, 9 | "digital_token": {:hex, :digital_token, "1.0.0", "454a4444061943f7349a51ef74b7fb1ebd19e6a94f43ef711f7dae88c09347df", [:mix], [{:cldr_utils, "~> 2.17", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "8ed6f5a8c2fa7b07147b9963db506a1b4c7475d9afca6492136535b064c9e9e6"}, 10 | "earmark": {:hex, :earmark, "1.4.14", "d04572cef64dd92726a97d92d714e38d6e130b024ea1b3f8a56e7de66ec04e50", [:mix], [{:earmark_parser, ">= 1.4.12", [hex: :earmark_parser, repo: "hexpm", optional: false]}], "hexpm", "df338b8b1852ee425180b276c56c6941cb12220e04fe8718fe4acbdd35fd699f"}, 11 | "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, 12 | "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"}, 13 | "ex_cldr": {:hex, :ex_cldr, "2.41.0", "b3d30e57e6a821d3d57d330dc4f8561ad07e0c70c41ad8f550b6420650e4f9ae", [:mix], [{:cldr_utils, "~> 2.28", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:gettext, "~> 0.19", [hex: :gettext, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: true]}], "hexpm", "9efc801ceed935120ca9d3d76cfe80d03abe00b9bca65400ec89d24bae53847d"}, 14 | "ex_cldr_calendars": {:hex, :ex_cldr_calendars, "2.1.0", "8c63140d02c30fe140c7cb8a149998fc625dfd7922019e0737de03316e628317", [:mix], [{:calendar_interval, "~> 0.2", [hex: :calendar_interval, repo: "hexpm", optional: true]}, {:ex_cldr_lists, "~> 2.10", [hex: :ex_cldr_lists, repo: "hexpm", optional: true]}, {:ex_cldr_numbers, "~> 2.34", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}, {:ex_cldr_units, "~> 3.18", [hex: :ex_cldr_units, repo: "hexpm", optional: true]}, {:ex_doc, "~> 0.21", [hex: :ex_doc, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "53608e65761de88d70cdf49288217a1b3b7b5049af559e1c8dece0aa751b9592"}, 15 | "ex_cldr_currencies": {:hex, :ex_cldr_currencies, "2.16.4", "d76770690699b6ba91f1fa253a299a905f9c22b45d91891b85f431b9dafa8b3b", [:mix], [{:ex_cldr, "~> 2.38", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "46a67d1387f14e836b1a24d831fa5f0904663b4f386420736f40a7d534e3cb9e"}, 16 | "ex_cldr_lists": {:hex, :ex_cldr_lists, "2.11.1", "ad18f861d7c5ca82aac6d173469c6a2339645c96790172ab0aa255b64fb7303b", [:mix], [{:ex_cldr_numbers, "~> 2.25", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}, {:ex_doc, "~> 0.18", [hex: :ex_doc, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "00161c04510ccb3f18b19a6b8562e50c21f1e9c15b8ff4c934bea5aad0b4ade2"}, 17 | "ex_cldr_numbers": {:hex, :ex_cldr_numbers, "2.34.0", "471a432e6ae77d3196c3dc092add81efa6b38364cbfc37e002e199ce6f3db784", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:digital_token, "~> 0.3 or ~> 1.0", [hex: :digital_token, repo: "hexpm", optional: false]}, {:ex_cldr, "~> 2.41", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:ex_cldr_currencies, "~> 2.16", [hex: :ex_cldr_currencies, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "37d9f66f3a3d6675715e95b59b91d40301d233894d41b8bb7e3a11ac6d24ba02"}, 18 | "ex_cldr_units": {:hex, :ex_cldr_units, "3.18.0", "da6906f923ca7a07e668dabe9d3700bfc86bfbe498d466d7ea105e7de47c7050", [:mix], [{:cldr_utils, "~> 2.25", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ex_cldr_lists, "~> 2.10", [hex: :ex_cldr_lists, repo: "hexpm", optional: false]}, {:ex_cldr_numbers, "~> 2.34.0", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}, {:ex_doc, "~> 0.18", [hex: :ex_doc, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "b9526723b9beb4528a28ddc9ce5e4df95044652cfc9161f99ef8e4edc8c898f8"}, 19 | "ex_doc": {:hex, :ex_doc, "0.37.3", "f7816881a443cd77872b7d6118e8a55f547f49903aef8747dbcb345a75b462f9", [:mix], [{:earmark_parser, "~> 1.4.42", [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", "e6aebca7156e7c29b5da4daa17f6361205b2ae5f26e5c7d8ca0d3f7e18972233"}, 20 | "exprintf": {:hex, :exprintf, "0.2.1", "b7e895dfb00520cfb7fc1671303b63b37dc3897c59be7cbf1ae62f766a8a0314", [:mix], [], "hexpm", "20a0e8c880be90e56a77fcc82533c5d60c643915c7ce0cc8aa1e06ed6001da28"}, 21 | "exprof": {:hex, :exprof, "0.2.4", "13ddc0575a6d24b52e7c6809d2a46e9ad63a4dd179628698cdbb6c1f6e497c98", [:mix], [{:exprintf, "~> 0.2", [hex: :exprintf, repo: "hexpm", optional: false]}], "hexpm", "0884bcb66afc421c75d749156acbb99034cc7db6d3b116c32e36f32551106957"}, 22 | "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, 23 | "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, 24 | "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [: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", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, 25 | "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"}, 26 | "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, 27 | "numbers": {:hex, :numbers, "5.2.4", "f123d5bb7f6acc366f8f445e10a32bd403c8469bdbce8ce049e1f0972b607080", [:mix], [{:coerce, "~> 1.0", [hex: :coerce, repo: "hexpm", optional: false]}, {:decimal, "~> 1.9 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "eeccf5c61d5f4922198395bf87a465b6f980b8b862dd22d28198c5e6fab38582"}, 28 | "ratio": {:hex, :ratio, "2.4.2", "c8518f3536d49b1b00d88dd20d49f8b11abb7819638093314a6348139f14f9f9", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:numbers, "~> 5.2.0", [hex: :numbers, repo: "hexpm", optional: false]}], "hexpm", "441ef6f73172a3503de65ccf1769030997b0d533b1039422f1e5e0e0b4cbf89e"}, 29 | "statistex": {:hex, :statistex, "1.0.0", "f3dc93f3c0c6c92e5f291704cf62b99b553253d7969e9a5fa713e5481cd858a5", [:mix], [], "hexpm", "ff9d8bee7035028ab4742ff52fc80a2aa35cece833cf5319009b52f1b5a86c27"}, 30 | "tz": {:hex, :tz, "0.28.1", "717f5ffddfd1e475e2a233e221dc0b4b76c35c4b3650b060c8e3ba29dd6632e9", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:mint, "~> 1.6", [hex: :mint, repo: "hexpm", optional: true]}], "hexpm", "bfdca1aa1902643c6c43b77c1fb0cb3d744fd2f09a8a98405468afdee0848c8a"}, 31 | } 32 | -------------------------------------------------------------------------------- /mix/for_dialyzer.ex: -------------------------------------------------------------------------------- 1 | defmodule Cldr.DatesTimes.Dialyzer do 2 | @moduledoc """ 3 | Functions just here to exercise dialyzer. 4 | 5 | This module is not included in the hex package. 6 | 7 | """ 8 | def backend_formats do 9 | {:ok, %{medium: _format_dt}} = MyApp.Cldr.DateTime.Format.date_time_formats("en") 10 | {:ok, %{medium: _format_dt}} = MyApp.Cldr.DateTime.Format.date_formats("en") 11 | {:ok, %{medium: _format_dt}} = MyApp.Cldr.DateTime.Format.time_formats("en") 12 | 13 | {:ok, %{medium: _format_dt}} = MyApp.Cldr.DateTime.Format.date_time_formats(:en) 14 | {:ok, %{medium: _format_dt}} = MyApp.Cldr.DateTime.Format.date_formats(:en) 15 | {:ok, %{medium: _format_dt}} = MyApp.Cldr.DateTime.Format.time_formats(:en) 16 | end 17 | 18 | def formats do 19 | {:ok, _} = Cldr.Date.formats() 20 | {:ok, _} = Cldr.Time.formats() 21 | {:ok, _} = Cldr.DateTime.formats() 22 | 23 | {:ok, _} = Cldr.Date.available_formats() 24 | {:ok, _} = Cldr.Time.available_formats() 25 | {:ok, _} = Cldr.DateTime.available_formats() 26 | end 27 | 28 | def format do 29 | _ = Cldr.DateTime.Format.calendars_for(:en, MyApp.Cldr) 30 | _ = Cldr.DateTime.Format.calendars_for("en", MyApp.Cldr) 31 | _ = MyApp.Cldr.DateTime.Format.calendars_for("en") 32 | 33 | _ = Cldr.DateTime.Format.gmt_format(:en, MyApp.Cldr) 34 | _ = Cldr.DateTime.Format.gmt_format("en", MyApp.Cldr) 35 | _ = MyApp.Cldr.DateTime.Format.gmt_format("en") 36 | 37 | _ = Cldr.DateTime.Format.gmt_zero_format(:en, MyApp.Cldr) 38 | _ = Cldr.DateTime.Format.gmt_zero_format("en", MyApp.Cldr) 39 | _ = MyApp.Cldr.DateTime.Format.gmt_zero_format("en") 40 | 41 | _ = Cldr.DateTime.Format.hour_format(:en, MyApp.Cldr) 42 | _ = Cldr.DateTime.Format.hour_format("en", MyApp.Cldr) 43 | _ = MyApp.Cldr.DateTime.Format.hour_format("en") 44 | 45 | _ = Cldr.Date.formats(:en, :buddhist, MyApp.Cldr) 46 | _ = Cldr.Date.formats("en", :buddhist, MyApp.Cldr) 47 | _ = MyApp.Cldr.DateTime.Format.date_formats("en", :buddhist) 48 | 49 | _ = Cldr.DateTime.Format.time_formats(:en, :buddhist) 50 | _ = Cldr.DateTime.Format.time_formats("en", :buddhist) 51 | _ = MyApp.Cldr.DateTime.Format.time_formats("en") 52 | 53 | _ = Cldr.Date.formats(:en, :buddhist) 54 | _ = Cldr.Date.formats("en", :buddhist) 55 | _ = MyApp.Cldr.DateTime.Format.date_formats("en", :buddhist) 56 | 57 | _ = Cldr.DateTime.Format.date_time_formats(:en, :buddhist) 58 | _ = Cldr.DateTime.Format.date_time_formats("en", :buddhist) 59 | _ = MyApp.Cldr.DateTime.Format.date_time_formats("en", :buddhist) 60 | 61 | _ = Cldr.DateTime.Format.date_time_available_formats(:en) 62 | _ = Cldr.DateTime.Format.date_time_available_formats("en") 63 | _ = MyApp.Cldr.DateTime.Format.date_time_available_formats("en") 64 | 65 | _ = Cldr.DateTime.Format.interval_formats(:en, :gregorian, MyApp.Cldr) 66 | _ = Cldr.DateTime.Format.interval_formats("en", :gregorian, MyApp.Cldr) 67 | _ = MyApp.Cldr.DateTime.Format.date_time_interval_formats("en", :gregorian) 68 | 69 | _ = Cldr.DateTime.Format.common_date_time_format_names() 70 | end 71 | 72 | def other_tests do 73 | datetime = DateTime.utc_now() 74 | Process.sleep(3000) 75 | 76 | _ = 77 | datetime 78 | |> DateTime.diff(DateTime.utc_now(), :second) 79 | |> MyApp.Cldr.DateTime.Relative.to_string!() 80 | 81 | _ = MyApp.Cldr.DateTime.to_string!(datetime, []) 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /mix/my_app_backend.ex: -------------------------------------------------------------------------------- 1 | defmodule MyApp.Cldr do 2 | use Cldr, 3 | locales: ["en", "fr", "af", "ja", "de", "pl", "th", "fa", "es", "da", "he", "en-CA", "en-AU", "en-GB"], 4 | providers: [Cldr.Number, Cldr.Calendar, Cldr.DateTime, Cldr.Unit, Cldr.List], 5 | precompile_number_formats: ["#,##0"], 6 | precompile_transliterations: [{:latn, :thai}] 7 | end 8 | -------------------------------------------------------------------------------- /src/date_time_format_lexer.xrl: -------------------------------------------------------------------------------- 1 | % Tokenizes CLDR date and time formats which are described at 2 | % http://unicode.org/reports/tr35/tr35-dates.html 3 | 4 | Definitions. 5 | 6 | Era = G 7 | 8 | YearNumeric = y 9 | YearWeek = Y 10 | YearExtended = u 11 | CyclicYear = U 12 | RelatedYear = r 13 | 14 | Quarter = q 15 | StandAloneQuarter = Q 16 | 17 | Month = M 18 | StandAloneMonth = L 19 | 20 | WeekOfYear = w 21 | WeekOfMonth = W 22 | 23 | DayOfMonth = d 24 | DayOfYear = D 25 | DayOfWeekInMonth = F 26 | 27 | WeekdayName = E 28 | WeekdayNumber = e 29 | StandAloneDayOfWeek = c 30 | 31 | Period_am_pm = a 32 | Period_noon_mid = b 33 | Period_flex = B 34 | 35 | Hour_0_11 = K 36 | Hour_1_12 = h 37 | Hour_0_23 = H 38 | Hour_1_24 = k 39 | 40 | Minute = m 41 | 42 | Second = s 43 | FractionalSecond = S 44 | Millisecond = A 45 | 46 | ShortZone = z 47 | BasicZone = Z 48 | GMT_Zone = O 49 | GenericZone = v 50 | ZoneID = V 51 | ISO_ZoneZ = X 52 | ISO_Zone = x 53 | 54 | Date = ({1}) 55 | Time = ({0}) 56 | 57 | Quote = '' 58 | Quoted = '[^']+' 59 | Char = [^a-zA-Z{}'] 60 | 61 | Rules. 62 | 63 | {Era}+ : {token,{era,TokenLine,count(TokenChars)}}. 64 | 65 | {YearNumeric}+ : {token,{year,TokenLine,count(TokenChars)}}. 66 | {YearWeek}+ : {token,{week_aligned_year,TokenLine,count(TokenChars)}}. 67 | {YearExtended}+ : {token,{extended_year,TokenLine,count(TokenChars)}}. 68 | {CyclicYear}+ : {token,{cyclic_year,TokenLine,count(TokenChars)}}. 69 | {RelatedYear}+ : {token,{related_year,TokenLine,count(TokenChars)}}. 70 | 71 | {Quarter}+ : {token,{quarter,TokenLine,count(TokenChars)}}. 72 | {StandAloneQuarter}+ : {token,{standalone_quarter,TokenLine,count(TokenChars)}}. 73 | 74 | {Time} : {token,{time,TokenLine,0}}. 75 | {Date} : {token,{date,TokenLine,0}}. 76 | 77 | {Month}+ : {token,{month,TokenLine,count(TokenChars)}}. 78 | {StandAloneMonth}+ : {token,{standalone_month,TokenLine,count(TokenChars)}}. 79 | 80 | {WeekOfYear}+ : {token,{week_of_year,TokenLine,count(TokenChars)}}. 81 | {WeekOfMonth}+ : {token,{week_of_month,TokenLine,count(TokenChars)}}. 82 | {DayOfMonth}+ : {token,{day_of_month,TokenLine,count(TokenChars)}}. 83 | {DayOfYear}+ : {token,{day_of_year,TokenLine,count(TokenChars)}}. 84 | {DayOfWeekInMonth}+ : {token,{day_of_week_in_month,TokenLine,count(TokenChars)}}. 85 | 86 | {WeekdayName}+ : {token,{day_name,TokenLine,count(TokenChars)}}. 87 | {WeekdayNumber}+ : {token,{day_of_week,TokenLine,count(TokenChars)}}. 88 | {StandAloneDayOfWeek}+ : {token,{standalone_day_of_week,TokenLine,count(TokenChars)}}. 89 | 90 | {Period_am_pm}+ : {token,{period_am_pm,TokenLine,count(TokenChars)}}. 91 | {Period_noon_mid}+ : {token,{period_noon_midnight,TokenLine,count(TokenChars)}}. 92 | {Period_flex}+ : {token,{period_flex,TokenLine,count(TokenChars)}}. 93 | 94 | {Hour_1_12}+ : {token,{h12,TokenLine,count(TokenChars)}}. 95 | {Hour_0_11}+ : {token,{h11,TokenLine,count(TokenChars)}}. 96 | {Hour_1_24}+ : {token,{h24,TokenLine,count(TokenChars)}}. 97 | {Hour_0_23}+ : {token,{h23,TokenLine,count(TokenChars)}}. 98 | 99 | {Minute}+ : {token,{minute,TokenLine,count(TokenChars)}}. 100 | {Second}+ : {token,{second,TokenLine,count(TokenChars)}}. 101 | {FractionalSecond}+ : {token,{fractional_second,TokenLine,count(TokenChars)}}. 102 | {Millisecond}+ : {token,{millisecond,TokenLine,count(TokenChars)}}. 103 | 104 | {ShortZone}+ : {token,{zone_short,TokenLine,count(TokenChars)}}. 105 | {BasicZone}+ : {token,{zone_basic,TokenLine,count(TokenChars)}}. 106 | {GMT_Zone}+ : {token,{zone_gmt,TokenLine,count(TokenChars)}}. 107 | {GenericZone}+ : {token,{zone_generic,TokenLine,count(TokenChars)}}. 108 | {ZoneID}+ : {token,{zone_id,TokenLine,count(TokenChars)}}. 109 | {ISO_ZoneZ}+ : {token,{zone_iso_z,TokenLine,count(TokenChars)}}. 110 | {ISO_Zone}+ : {token,{zone_iso,TokenLine,count(TokenChars)}}. 111 | 112 | {Quoted} : {token,{literal,TokenLine,'Elixir.List':to_string(unquote(TokenChars))}}. 113 | {Quote} : {token,{literal,TokenLine,<<"'">>}}. 114 | {Char}+ : {token,{literal,TokenLine,'Elixir.List':to_string(TokenChars)}}. 115 | 116 | Erlang code. 117 | 118 | -import('Elixir.List', [to_string/1]). 119 | 120 | count(Chars) -> string:len(Chars). 121 | 122 | unquote([_ | Tail]) -> 123 | [_ | Rev] = lists:reverse(Tail), 124 | lists:reverse(Rev). 125 | -------------------------------------------------------------------------------- /src/skeleton_tokenizer.xrl: -------------------------------------------------------------------------------- 1 | % Tokenizes CLDR date and time formats which are described at 2 | % http://unicode.org/reports/tr35/tr35-dates.html 3 | 4 | 5 | Definitions. 6 | 7 | Era = G 8 | 9 | YearNumeric = y 10 | YearWeek = Y 11 | YearExtended = u 12 | CyclicYear = U 13 | RelatedYear = r 14 | 15 | Quarter = q 16 | StandAloneQuarter = Q 17 | 18 | Month = M 19 | StandAloneMonth = L 20 | 21 | WeekOfYear = w 22 | WeekOfMonth = W 23 | 24 | DayOfMonth = d 25 | DayOfYear = D 26 | DayOfWeekInMonth = F 27 | 28 | WeekdayName = E 29 | WeekdayNumber = e 30 | StandAloneDayOfWeek = c 31 | 32 | Period_am_pm = a 33 | Period_noon_mid = b 34 | Period_flex = B 35 | 36 | Hour_0_11 = K 37 | Hour_1_12 = h 38 | Hour_0_23 = H 39 | Hour_1_24 = k 40 | 41 | Minute = m 42 | 43 | Second = s 44 | FractionalSecond = S 45 | Millisecond = A 46 | 47 | ShortZone = z 48 | BasicZone = Z 49 | GMT_Zone = O 50 | GenericZone = v 51 | ZoneID = V 52 | ISO_ZoneZ = X 53 | ISO_Zone = x 54 | 55 | Skeleton_j = j 56 | Skeleton_J = J 57 | Skeleton_C = C 58 | 59 | Rules. 60 | 61 | {Era}+ : {token, {symbol(TokenChars), count(TokenChars)}}. 62 | 63 | {YearNumeric}+ : {token, {symbol(TokenChars), count(TokenChars)}}. 64 | {YearWeek}+ : {token, {symbol(TokenChars), count(TokenChars)}}. 65 | {YearExtended}+ : {token, {symbol(TokenChars), count(TokenChars)}}. 66 | {CyclicYear}+ : {token, {symbol(TokenChars), count(TokenChars)}}. 67 | {RelatedYear}+ : {token, {symbol(TokenChars), count(TokenChars)}}. 68 | 69 | {Quarter}+ : {token, {symbol(TokenChars), count(TokenChars)}}. 70 | {StandAloneQuarter}+ : {token, {symbol(TokenChars), count(TokenChars)}}. 71 | 72 | {Month}+ : {token, {symbol(TokenChars), count(TokenChars)}}. 73 | {StandAloneMonth}+ : {token, {symbol(TokenChars), count(TokenChars)}}. 74 | 75 | {WeekOfYear}+ : {token, {symbol(TokenChars), count(TokenChars)}}. 76 | {WeekOfMonth}+ : {token, {symbol(TokenChars), count(TokenChars)}}. 77 | {DayOfMonth}+ : {token, {symbol(TokenChars), count(TokenChars)}}. 78 | {DayOfYear}+ : {token, {symbol(TokenChars), count(TokenChars)}}. 79 | {DayOfWeekInMonth}+ : {token, {symbol(TokenChars), count(TokenChars)}}. 80 | 81 | {WeekdayName}+ : {token, {symbol(TokenChars), count(TokenChars)}}. 82 | {WeekdayNumber}+ : {token, {symbol(TokenChars), count(TokenChars)}}. 83 | {StandAloneDayOfWeek}+ : {token, {symbol(TokenChars), count(TokenChars)}}. 84 | 85 | {Period_am_pm}+ : {token, {symbol(TokenChars), count(TokenChars)}}. 86 | {Period_noon_mid}+ : {token, {symbol(TokenChars), count(TokenChars)}}. 87 | {Period_flex}+ : {token, {symbol(TokenChars), count(TokenChars)}}. 88 | 89 | {Hour_1_12}+ : {token, {symbol(TokenChars), count(TokenChars)}}. 90 | {Hour_0_11}+ : {token, {symbol(TokenChars), count(TokenChars)}}. 91 | {Hour_1_24}+ : {token, {symbol(TokenChars), count(TokenChars)}}. 92 | {Hour_0_23}+ : {token, {symbol(TokenChars), count(TokenChars)}}. 93 | 94 | {Minute}+ : {token, {symbol(TokenChars), count(TokenChars)}}. 95 | {Second}+ : {token, {symbol(TokenChars), count(TokenChars)}}. 96 | {FractionalSecond}+ : {token, {symbol(TokenChars), count(TokenChars)}}. 97 | {Millisecond}+ : {token, {symbol(TokenChars), count(TokenChars)}}. 98 | 99 | {ShortZone}+ : {token, {symbol(TokenChars), count(TokenChars)}}. 100 | {BasicZone}+ : {token, {symbol(TokenChars), count(TokenChars)}}. 101 | {GMT_Zone}+ : {token, {symbol(TokenChars), count(TokenChars)}}. 102 | {GenericZone}+ : {token, {symbol(TokenChars), count(TokenChars)}}. 103 | {ZoneID}+ : {token, {symbol(TokenChars), count(TokenChars)}}. 104 | {ISO_ZoneZ}+ : {token, {symbol(TokenChars), count(TokenChars)}}. 105 | {ISO_Zone}+ : {token, {symbol(TokenChars), count(TokenChars)}}. 106 | 107 | {Skeleton_j}+ : {token, {symbol(TokenChars), count(TokenChars)}}. 108 | {Skeleton_J}+ : {token, {symbol(TokenChars), count(TokenChars)}}. 109 | {Skeleton_C}+ : {token, {symbol(TokenChars), count(TokenChars)}}. 110 | 111 | % This will never match. But without them, Dialyzer will report 112 | % a pattern_match error 113 | {Time} : {token, {symbol(TokenChars), 0}}. 114 | 115 | Erlang code. 116 | 117 | count(Chars) -> string:len(Chars). 118 | 119 | symbol(Chars) -> list_to_binary([hd(Chars)]). 120 | 121 | -------------------------------------------------------------------------------- /test/backend_doc_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Cldr.DateTime.Backend.Test do 2 | use ExUnit.Case 3 | 4 | doctest MyApp.Cldr.DateTime.Format 5 | doctest MyApp.Cldr.DateTime.Formatter 6 | end 7 | -------------------------------------------------------------------------------- /test/cldr_chars_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Cldr.DateTime.CharsTest do 2 | use ExUnit.Case, async: true 3 | 4 | test "date to_string" do 5 | assert Cldr.to_string(~D[2020-04-09]) == "Apr 9, 2020" 6 | end 7 | 8 | test "time to_string" do 9 | assert Cldr.to_string(~T[11:45:23]) == "11:45:23 AM" 10 | end 11 | 12 | test "naive datetime to_string" do 13 | assert Cldr.to_string(~N[2020-04-09 23:39:25]) == "Apr 9, 2020, 11:39:25 PM" 14 | end 15 | 16 | if Version.match?(System.version(), "~> 1.9") do 17 | test "datetime to_string" do 18 | assert Cldr.to_string(~U[2020-04-09 23:39:25.040129Z]) == "Apr 9, 2020, 11:39:25 PM" 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test/cldr_dates_times_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Cldr.DatesTimes.Test do 2 | use ExUnit.Case 3 | 4 | test "that the bb format works as expected" do 5 | assert Cldr.DateTime.to_string( 6 | %{ 7 | year: 2018, 8 | month: 1, 9 | day: 1, 10 | hour: 0, 11 | minute: 0, 12 | second: 0, 13 | calendar: Calendar.ISO 14 | }, 15 | MyApp.Cldr, 16 | format: "YYYY-MMM-dd KK:mm bb" 17 | ) == {:ok, "2018-Jan-01 00:00 midnight"} 18 | 19 | assert Cldr.DateTime.to_string( 20 | %{ 21 | year: 2018, 22 | month: 1, 23 | day: 1, 24 | hour: 0, 25 | minute: 1, 26 | second: 0, 27 | calendar: Calendar.ISO 28 | }, 29 | MyApp.Cldr, 30 | format: "YYYY-MMM-dd KK:mm bb" 31 | ) == {:ok, "2018-Jan-01 00:01 AM"} 32 | end 33 | 34 | test "That localised date doesn't transliterate" do 35 | assert Cldr.Date.to_string(~D[2019-06-12], MyApp.Cldr, locale: "de") == {:ok, "12.06.2019"} 36 | end 37 | 38 | test "Formatting via a backend when there is no default backend" do 39 | default_backend = Application.get_env(:ex_cldr, :default_backend) 40 | Application.put_env(:ex_cldr, :default_backend, nil) 41 | assert match?({:ok, _now}, MyApp.Cldr.DateTime.to_string(DateTime.utc_now())) 42 | assert match?({:ok, _now}, MyApp.Cldr.Date.to_string(Date.utc_today())) 43 | assert match?({:ok, _now}, MyApp.Cldr.Time.to_string(DateTime.utc_now())) 44 | Application.put_env(:ex_cldr, :default_backend, default_backend) 45 | end 46 | 47 | test "to_string/2 when the second param is options (not backend)" do 48 | assert Cldr.Date.to_string(~D[2022-01-22], backend: MyApp.Cldr) == {:ok, "Jan 22, 2022"} 49 | 50 | assert Cldr.DateTime.to_string(~U[2022-01-22T01:00:00.0Z], backend: MyApp.Cldr) == 51 | {:ok, "Jan 22, 2022, 1:00:00 AM"} 52 | 53 | assert Cldr.Time.to_string(~T[01:23:00], backend: MyApp.Cldr) == {:ok, "1:23:00 AM"} 54 | 55 | assert Cldr.Date.to_string!(~D[2022-01-22], backend: MyApp.Cldr) == "Jan 22, 2022" 56 | 57 | assert Cldr.DateTime.to_string!(~U[2022-01-22T01:00:00.0Z], backend: MyApp.Cldr) == 58 | "Jan 22, 2022, 1:00:00 AM" 59 | 60 | assert Cldr.Time.to_string!(~T[01:23:00], backend: MyApp.Cldr) == "1:23:00 AM" 61 | end 62 | 63 | test "DateTime at formats" do 64 | date_time = ~U[2023-09-08 15:50:00Z] 65 | 66 | assert Cldr.DateTime.to_string(date_time, format: :full, style: :at) == 67 | {:ok, "Friday, September 8, 2023 at 3:50:00 PM GMT"} 68 | 69 | assert Cldr.DateTime.to_string(date_time, format: :long, style: :at) == 70 | {:ok, "September 8, 2023 at 3:50:00 PM UTC"} 71 | 72 | assert Cldr.DateTime.to_string(date_time, format: :full, style: :at, locale: :fr) == 73 | {:ok, "vendredi 8 septembre 2023 à 15:50:00 UTC"} 74 | 75 | assert Cldr.DateTime.to_string(date_time, format: :long, style: :at, locale: :fr) == 76 | {:ok, "8 septembre 2023 à 15:50:00 UTC"} 77 | end 78 | 79 | test "Era variants" do 80 | assert {:ok, "2024/7/6 CE"} = 81 | Cldr.Date.to_string(~D[2024-07-06], era: :variant, format: "y/M/d G") 82 | 83 | assert {:ok, "2024/7/6 AD"} = Cldr.Date.to_string(~D[2024-07-06], format: "y/M/d G") 84 | end 85 | 86 | test "Resolving with skeleton code c, J and j" do 87 | assert {:ok, "10:48 AM"} = Cldr.Time.to_string(~T[10:48:00], format: :hmj) 88 | assert {:ok, "10:48 AM"} = Cldr.Time.to_string(~T[10:48:00], format: :hmJ) 89 | 90 | assert Cldr.Date.to_string(~T"10:48:00", format: :hmc) == 91 | {:error, 92 | { 93 | ArgumentError, 94 | "Missing required date fields. The function requires a map with at least :year, :month, :day and :calendar. " <> 95 | "Found: ~T[10:48:00 Cldr.Calendar.Gregorian]"}} 96 | 97 | assert Cldr.Time.to_string(~T[10:48:00], format: :hme) == 98 | {:error, {Cldr.DateTime.UnresolvedFormat, "No available format resolved for :hme"}} 99 | end 100 | 101 | test "Datetime formatting with standard formats" do 102 | datetime = ~U[2024-07-07 21:36:00.440105Z] 103 | 104 | assert {:ok, "Jul 7, 2024, 9:36:00 PM"} = 105 | Cldr.DateTime.to_string(datetime) 106 | 107 | assert {:ok, "7/7/24, 9:36:00 PM GMT"} = 108 | Cldr.DateTime.to_string(datetime, date_format: :short, time_format: :full) 109 | 110 | assert {:ok, "7/7/24, 9:36:00 PM GMT"} = 111 | Cldr.DateTime.to_string(datetime, 112 | format: :medium, 113 | date_format: :short, 114 | time_format: :full 115 | ) 116 | end 117 | 118 | test "Datetime format option consistency" do 119 | datetime = ~U[2024-07-07 21:36:00.440105Z] 120 | 121 | assert Cldr.DateTime.to_string(datetime, 122 | format: "yyy", 123 | date_format: :short, 124 | time_format: :medium 125 | ) == 126 | {:error, 127 | {Cldr.DateTime.InvalidFormat, 128 | ":date_format and :time_format cannot be specified if :format is also specified as a " <> 129 | "format id or a format string. Found [time_format: :medium, date_format: :short]"}} 130 | 131 | assert Cldr.DateTime.to_string(datetime, 132 | format: :yMd, 133 | date_format: :short, 134 | time_format: :medium 135 | ) == 136 | {:error, 137 | {Cldr.DateTime.InvalidFormat, 138 | ":date_format and :time_format cannot be specified if :format is also specified as a " <> 139 | "format id or a format string. Found [time_format: :medium, date_format: :short]"}} 140 | end 141 | 142 | test "Pluralized formats" do 143 | datetime = ~U[2024-07-07 21:36:00.440105Z] 144 | 145 | assert {:ok, "week 28 of 2024"} = Cldr.DateTime.to_string(~D[2024-07-08], format: :yw) 146 | assert {:ok, "week 2 of July"} = Cldr.DateTime.to_string(~D[2024-07-08], format: :MMMMW) 147 | assert {:ok, "8:11 AM"} = Cldr.DateTime.to_string(~T[08:11:02], format: :hm) 148 | assert {:ok, "Sun 9:36 PM"} = Cldr.DateTime.to_string(datetime, format: :Ehm) 149 | end 150 | 151 | test "'at' formats" do 152 | datetime = ~U[2024-07-07 21:36:00.440105Z] 153 | 154 | assert {:ok, "July 7, 2024 at 9:36:00 PM UTC"} = 155 | Cldr.DateTime.to_string(datetime, format: :long, style: :at) 156 | 157 | assert {:ok, "July 7, 2024, 9:36:00 PM UTC"} = 158 | Cldr.DateTime.to_string(datetime, format: :long, style: :default) 159 | end 160 | 161 | test "Symmetry of the format/3 and available_format/3 functions for Date, Time and DateTime" do 162 | assert {:ok, _} = Cldr.Date.formats() 163 | assert {:ok, _} = Cldr.Time.formats() 164 | assert {:ok, _} = Cldr.DateTime.formats() 165 | 166 | assert {:ok, _} = Cldr.Date.available_formats() 167 | assert {:ok, _} = Cldr.Time.available_formats() 168 | assert {:ok, _} = Cldr.DateTime.available_formats() 169 | end 170 | 171 | test "When to_string options is not a list" do 172 | assert {:error, 173 | {ArgumentError, 174 | "Unexpected option value \"en-GB\". Options must be a keyword list"}} = Cldr.DateTime.to_string DateTime.utc_now(), "en-GB" 175 | 176 | assert {:error, 177 | {ArgumentError, 178 | "Unexpected option value \"en-GB\". Options must be a keyword list"}} = Cldr.Date.to_string Date.utc_today(), "en-GB" 179 | 180 | assert {:error, 181 | {ArgumentError, 182 | "Unexpected option value \"en-GB\". Options must be a keyword list"}} = Cldr.Time.to_string Time.utc_now(), "en-GB" 183 | end 184 | end 185 | -------------------------------------------------------------------------------- /test/date_time_relative_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Cldr.DateTime.Relative.Test do 2 | use ExUnit.Case, async: true 3 | 4 | @date ~D[2021-10-01] 5 | @relative_to ~D[2021-09-19] 6 | 7 | @datetime ~U[2021-10-01 10:15:00+00:00] 8 | @relative_datetime_to ~U[2021-09-19 12:15:00+00:00] 9 | 10 | alias MyApp.Cldr.DateTime.Relative 11 | 12 | test "Relative dates with specified unit" do 13 | assert Relative.to_string(@date, relative_to: @relative_to) == 14 | {:ok, "in 2 weeks"} 15 | 16 | assert Relative.to_string(@date, relative_to: @relative_to, unit: :day) == 17 | {:ok, "in 12 days"} 18 | 19 | assert Relative.to_string(@date, relative_to: @relative_to, unit: :month) == 20 | {:ok, "this month"} 21 | 22 | assert Relative.to_string(@date, relative_to: @relative_to, unit: :week) == 23 | {:ok, "in 2 weeks"} 24 | 25 | assert Relative.to_string(@date, relative_to: @relative_to, unit: :hour) == 26 | {:ok, "in 288 hours"} 27 | 28 | assert Relative.to_string(@date, relative_to: @relative_to, unit: :minute) == 29 | {:ok, "in 17,280 minutes"} 30 | 31 | assert Relative.to_string(@date, relative_to: @relative_to, unit: :second) == 32 | {:ok, "in 1,036,800 seconds"} 33 | 34 | assert Relative.to_string(@date, relative_to: @relative_to, unit: :year) == 35 | {:ok, "this year"} 36 | end 37 | 38 | test "Relative datetime with specified unit" do 39 | assert Relative.to_string(@datetime, relative_to: @relative_datetime_to) == 40 | {:ok, "in 2 weeks"} 41 | 42 | assert Relative.to_string(@datetime, relative_to: @relative_datetime_to, unit: :day) == 43 | {:ok, "in 12 days"} 44 | 45 | assert Relative.to_string(@datetime, relative_to: @relative_datetime_to, unit: :month) == 46 | {:ok, "this month"} 47 | 48 | assert Relative.to_string(@datetime, relative_to: @relative_datetime_to, unit: :week) == 49 | {:ok, "in 2 weeks"} 50 | 51 | assert Relative.to_string(@datetime, relative_to: @relative_datetime_to, unit: :hour) == 52 | {:ok, "in 286 hours"} 53 | 54 | assert Relative.to_string(@datetime, relative_to: @relative_datetime_to, unit: :minute) == 55 | {:ok, "in 17,160 minutes"} 56 | 57 | assert Relative.to_string(@datetime, relative_to: @relative_datetime_to, unit: :second) == 58 | {:ok, "in 1,029,600 seconds"} 59 | 60 | assert Relative.to_string(@datetime, relative_to: @relative_datetime_to, unit: :year) == 61 | {:ok, "this year"} 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /test/doc_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Cldr.DateTime.Test do 2 | use ExUnit.Case 3 | 4 | doctest Cldr.DateTime.Relative 5 | doctest Cldr.DateTime.Format.Compiler 6 | doctest Cldr.DateTime.Formatter 7 | doctest Cldr.DateTime.Format 8 | doctest Cldr.DateTime 9 | doctest Cldr.Date 10 | doctest Cldr.Time 11 | 12 | doctest Cldr.Interval 13 | doctest Cldr.DateTime.Interval 14 | doctest Cldr.Date.Interval 15 | doctest Cldr.Time.Interval 16 | 17 | doctest MyApp.Cldr.Date 18 | doctest MyApp.Cldr.Time 19 | doctest MyApp.Cldr.DateTime 20 | doctest MyApp.Cldr.DateTime.Relative 21 | 22 | doctest MyApp.Cldr.Interval 23 | doctest MyApp.Cldr.DateTime.Interval 24 | doctest MyApp.Cldr.Date.Interval 25 | doctest MyApp.Cldr.Time.Interval 26 | end 27 | -------------------------------------------------------------------------------- /test/duration_format_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Cldr.DateTime.DurationFormatTest do 2 | use ExUnit.Case, async: true 3 | 4 | test "Formatting a Cldr.Calendar.Duration" do 5 | duration = Cldr.Calendar.Duration.new_from_seconds 136092 6 | assert {:ok, "37:48:12"} = Cldr.Time.to_string(duration, format: "hh:mm:ss") 7 | end 8 | 9 | if Code.ensure_loaded?(Duration) do 10 | test "Formatting a Duration" do 11 | duration = Duration.new!(hour: 28, minute: 15, second: 6) 12 | assert {:ok, "28:15:06"} = Cldr.Time.to_string(duration, format: "hh:mm:ss") 13 | end 14 | end 15 | end -------------------------------------------------------------------------------- /test/exceptions_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Cldr.Exceptions.Test do 2 | use ExUnit.Case, async: true 3 | 4 | test "that an invalid datetime raises" do 5 | assert_raise ArgumentError, 6 | ~r/Invalid DateTime. DateTime is a map that contains at least .*/, 7 | fn -> 8 | Cldr.DateTime.to_string!("not a date") 9 | end 10 | end 11 | 12 | test "that an invalid date raises" do 13 | assert_raise ArgumentError, ~r/Missing required date fields. .*/, fn -> 14 | Cldr.Date.to_string!("not a date") 15 | end 16 | end 17 | 18 | test "that an invalid time raises" do 19 | assert_raise ArgumentError, ~r/Invalid time. Time is a map that contains at least .*/, fn -> 20 | Cldr.Time.to_string!("not a time") 21 | end 22 | end 23 | 24 | if Version.compare(System.version(), "1.10.0-dev") in [:gt, :eq] do 25 | test "that an unfulfilled format directive returns an error" do 26 | assert Cldr.Date.to_string(~D[2019-01-01], format: "x") == 27 | {:error, 28 | {Cldr.DateTime.FormatError, 29 | "The format symbol 'x' requires at map with at least :utc_offset. Found: ~D[2019-01-01 Cldr.Calendar.Gregorian]"}} 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /test/interval_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Cldr.DateTime.Interval.Test do 2 | use ExUnit.Case, async: true 3 | 4 | test "date formatting" do 5 | assert Cldr.Date.Interval.to_string(~D[2020-01-01], ~D[2020-02-01]) 6 | assert Cldr.Date.Interval.to_string(~D[2020-01-01], ~D[2020-02-01], MyApp.Cldr) 7 | assert Cldr.Date.Interval.to_string(~D[2020-01-01], ~D[2020-02-01], locale: "fr") 8 | assert Cldr.Date.Interval.to_string(~D[2020-01-01], ~D[2020-02-01], MyApp.Cldr, locale: "fr") 9 | end 10 | 11 | test "right open date interval" do 12 | assert Cldr.Date.Interval.to_string(~D[2020-01-01], nil) == {:ok, "Jan 1, 2020 –"} 13 | assert Cldr.Date.Interval.to_string(~D[2020-01-01], nil, MyApp.Cldr) == {:ok, "Jan 1, 2020 –"} 14 | 15 | assert Cldr.Date.Interval.to_string(~D[2020-01-01], nil, locale: "fr") == 16 | {:ok, "1 janv. 2020 –"} 17 | 18 | assert Cldr.Date.Interval.to_string(~D[2020-01-01], nil, MyApp.Cldr, locale: "fr") == 19 | {:ok, "1 janv. 2020 –"} 20 | end 21 | 22 | test "left open date interval" do 23 | assert Cldr.Date.Interval.to_string(nil, ~D[2020-01-01]) == {:ok, "– Jan 1, 2020"} 24 | assert Cldr.Date.Interval.to_string(nil, ~D[2020-01-01], MyApp.Cldr) == {:ok, "– Jan 1, 2020"} 25 | 26 | assert Cldr.Date.Interval.to_string(nil, ~D[2020-01-01], locale: "fr") == 27 | {:ok, "– 1 janv. 2020"} 28 | 29 | assert Cldr.Date.Interval.to_string(nil, ~D[2020-01-01], MyApp.Cldr, locale: "fr") == 30 | {:ok, "– 1 janv. 2020"} 31 | end 32 | 33 | test "time formatting" do 34 | assert Cldr.Time.Interval.to_string(~T[00:00:00.0], ~T[10:00:00.0]) 35 | assert Cldr.Time.Interval.to_string(~T[00:00:00.0], ~T[10:00:00.0], MyApp.Cldr) 36 | assert Cldr.Time.Interval.to_string(~T[00:00:00.0], ~T[10:00:00.0], locale: "fr") 37 | assert Cldr.Time.Interval.to_string(~T[00:00:00.0], ~T[10:00:00.0], MyApp.Cldr, locale: "fr") 38 | 39 | assert Cldr.Time.Interval.to_string(~T[10:00:00], ~T[22:03:00], MyApp.Cldr, 40 | format: :short, 41 | locale: "en-GB" 42 | ) == {:ok, "10–22"} 43 | end 44 | 45 | test "right option time interval" do 46 | assert Cldr.Time.Interval.to_string(~T[00:00:00.0], nil) == {:ok, "12:00:00 AM –"} 47 | assert Cldr.Time.Interval.to_string(~T[00:00:00.0], nil, MyApp.Cldr) == {:ok, "12:00:00 AM –"} 48 | assert Cldr.Time.Interval.to_string(~T[00:00:00.0], nil, locale: "fr") == {:ok, "00:00:00 –"} 49 | 50 | assert Cldr.Time.Interval.to_string(~T[00:00:00.0], nil, MyApp.Cldr, locale: "fr") == 51 | {:ok, "00:00:00 –"} 52 | end 53 | 54 | test "left option time interval" do 55 | assert Cldr.Time.Interval.to_string(nil, ~T[00:00:00.0]) == {:ok, "– 12:00:00 AM"} 56 | assert Cldr.Time.Interval.to_string(nil, ~T[00:00:00.0], MyApp.Cldr) == {:ok, "– 12:00:00 AM"} 57 | assert Cldr.Time.Interval.to_string(nil, ~T[00:00:00.0], locale: "fr") == {:ok, "– 00:00:00"} 58 | 59 | assert Cldr.Time.Interval.to_string(nil, ~T[00:00:00.0], MyApp.Cldr, locale: "fr") == 60 | {:ok, "– 00:00:00"} 61 | end 62 | 63 | # Just to get tests compiling. Those tests will 64 | # then be omitted by the tag 65 | unless Version.match?(System.version(), "~> 1.9") do 66 | defmacrop sigil_U(string, _options) do 67 | string 68 | end 69 | end 70 | 71 | @tag :elixir_1_9 72 | test "datetime formatting" do 73 | assert Cldr.DateTime.Interval.to_string(~U[2020-01-01 00:00:00.0Z], ~U[2020-02-01 10:00:00.0Z]) 74 | 75 | assert Cldr.DateTime.Interval.to_string( 76 | ~U[2020-01-01 00:00:00.0Z], 77 | ~U[2020-02-01 10:00:00.0Z], 78 | MyApp.Cldr 79 | ) 80 | 81 | assert Cldr.DateTime.Interval.to_string(~U[2020-01-01 00:00:00.0Z], ~U[2020-02-01 10:00:00.0Z], 82 | locale: "fr" 83 | ) 84 | 85 | assert Cldr.DateTime.Interval.to_string( 86 | ~U[2020-01-01 00:00:00.0Z], 87 | ~U[2020-02-01 10:00:00.0Z], 88 | MyApp.Cldr, 89 | locale: "fr" 90 | ) 91 | 92 | assert Cldr.DateTime.Interval.to_string( 93 | ~U[2020-01-01 00:00:00.0Z], 94 | nil, 95 | MyApp.Cldr, 96 | locale: "fr" 97 | ) == {:ok, "1 janv. 2020, 00:00:00 –"} 98 | 99 | assert Cldr.DateTime.Interval.to_string( 100 | nil, 101 | ~U[2020-01-01 00:00:00.0Z], 102 | MyApp.Cldr, 103 | locale: "fr" 104 | ) == {:ok, "– 1 janv. 2020, 00:00:00"} 105 | end 106 | 107 | test "backend date formatting" do 108 | assert MyApp.Cldr.Date.Interval.to_string(~D[2020-01-01], ~D[2020-02-01]) 109 | assert MyApp.Cldr.Date.Interval.to_string(~D[2020-01-01], ~D[2020-02-01], locale: "fr") 110 | end 111 | 112 | test "backend time formatting" do 113 | assert MyApp.Cldr.Time.Interval.to_string(~T[00:00:00.0], ~T[10:00:00.0]) 114 | assert MyApp.Cldr.Time.Interval.to_string(~T[00:00:00.0], ~T[10:00:00.0], locale: "fr") 115 | end 116 | 117 | @tag :elixir_1_9 118 | test "backend datetime formatting" do 119 | assert MyApp.Cldr.DateTime.Interval.to_string( 120 | ~U[2020-01-01 00:00:00.0Z], 121 | ~U[2020-02-01 10:00:00.0Z] 122 | ) 123 | 124 | assert MyApp.Cldr.DateTime.Interval.to_string( 125 | ~U[2020-01-01 00:00:00.0Z], 126 | ~U[2020-02-01 10:00:00.0Z], 127 | locale: "fr" 128 | ) 129 | end 130 | 131 | test "Error returns" do 132 | assert Cldr.Date.Interval.to_string(Date.range(~D[2020-01-01], ~D[2020-01-12]), MyApp.Cldr, 133 | number_system: "unknown" 134 | ) == 135 | {:error, {Cldr.UnknownNumberSystemError, "The number system \"unknown\" is invalid"}} 136 | 137 | assert Cldr.Date.Interval.to_string(Date.range(~D[2020-01-01], ~D[2020-01-12]), MyApp.Cldr, 138 | locale: "unknown" 139 | ) == 140 | {:error, {Cldr.InvalidLanguageError, "The language \"unknown\" is invalid"}} 141 | 142 | assert Cldr.Date.Interval.to_string(Date.range(~D[2020-01-01], ~D[2020-01-12]), MyApp.Cldr, 143 | format: "unknown" 144 | ) == 145 | {:error, 146 | {Cldr.DateTime.Compiler.ParseError, 147 | "Could not tokenize \"unk\". Error detected at " <> inspect([?n])}} 148 | 149 | assert Cldr.Date.Interval.to_string(Date.range(~D[2020-01-01], ~D[2020-01-12]), MyApp.Cldr, 150 | format: :unknown 151 | ) == 152 | {:error, 153 | {Cldr.DateTime.UnresolvedFormat, 154 | "The interval format :unknown is invalid. Valid formats are [:long, :medium, :short] or an interval format string."}} 155 | 156 | assert Cldr.Date.Interval.to_string(Date.range(~D[2020-01-01], ~D[2020-01-12]), MyApp.Cldr, 157 | style: :unknown 158 | ) == 159 | {:error, 160 | {Cldr.DateTime.InvalidStyle, 161 | "The interval style :unknown is invalid. Valid styles are [:date, :month, :month_and_day, :year_and_month]."}} 162 | 163 | assert Cldr.Date.Interval.to_string(Date.range(~D[2020-01-01], ~D[2020-01-12]), MyApp.Cldr, 164 | style: "unknown" 165 | ) == 166 | {:error, 167 | {Cldr.DateTime.InvalidStyle, 168 | "The interval style \"unknown\" is invalid. Valid styles are [:date, :month, :month_and_day, :year_and_month]."}} 169 | end 170 | 171 | test "time intervals that cross midday" do 172 | assert Cldr.Time.Interval.to_string!(~T[08:00:00], ~T[22:00:00]) == "8:00 AM – 10:00 PM" 173 | assert Cldr.Time.Interval.to_string!(~T[20:00:00], ~T[22:00:00]) == "8:00 – 10:00 PM" 174 | end 175 | 176 | test "time intervals obey locale's hour format" do 177 | assert {:ok, "12:00 – 13:00"} = 178 | Cldr.Time.Interval.to_string(~T[12:00:00], ~T[13:00:00], MyApp.Cldr, locale: :fr) 179 | 180 | assert {:ok, "12:00 – 1:00 PM"} = 181 | Cldr.Time.Interval.to_string(~T[12:00:00], ~T[13:00:00], MyApp.Cldr, locale: :en) 182 | end 183 | 184 | test "Interval formatting when the format is a string" do 185 | assert {:ok, "12:00:00 - 13:00:00"} = 186 | MyApp.Cldr.Time.Interval.to_string(~T[12:00:00], ~T[13:00:00], 187 | format: "HH:mm:ss - HH:mm:ss", 188 | locale: :fr 189 | ) 190 | end 191 | 192 | test "Interval formatting of dates with :month_and_day where the last date is in a subsequent year" do 193 | assert {:ok, "12/31 – 1/2"} = 194 | MyApp.Cldr.Date.Interval.to_string(~D[2023-12-31], ~D[2024-01-02], 195 | format: :short, 196 | style: :month_and_day 197 | ) 198 | end 199 | 200 | test "Interval formats with different date and time formats" do 201 | assert {:ok, "January 1, 2020, 12:00 AM – December 31, 2020, 10:00 AM"} = 202 | MyApp.Cldr.DateTime.Interval.to_string( 203 | ~U[2020-01-01 00:00:00.0Z], 204 | ~U[2020-12-31 10:00:00.0Z], 205 | format: :medium, 206 | date_format: :long, 207 | time_format: :short 208 | ) 209 | 210 | assert {:ok, "January 1, 2020, 12:00 AM – 10:00 AM"} = 211 | MyApp.Cldr.DateTime.Interval.to_string( 212 | ~U[2020-01-01 00:00:00.0Z], 213 | ~U[2020-01-01 10:00:00.0Z], 214 | format: :medium, 215 | date_format: :long, 216 | time_format: :short 217 | ) 218 | 219 | assert {:ok, "1/1/20, 12:00 AM – 12/31/20, 10:00 AM"} = 220 | MyApp.Cldr.DateTime.Interval.to_string( 221 | ~U[2020-01-01 00:00:00.0Z], 222 | ~U[2020-12-31 10:00:00.0Z], 223 | format: :medium, 224 | date_format: :short, 225 | time_format: :short 226 | ) 227 | 228 | assert {:ok, "1/1/20, 12:00 AM – 10:00 AM"} = 229 | MyApp.Cldr.DateTime.Interval.to_string( 230 | ~U[2020-01-01 00:00:00.0Z], 231 | ~U[2020-01-01 10:00:00.0Z], 232 | format: :medium, 233 | date_format: :short, 234 | time_format: :short 235 | ) 236 | end 237 | 238 | test "Invalid :date_format and :time_format for intervals" do 239 | assert {:error, 240 | {Cldr.DateTime.InvalidFormat, 241 | ":date_format and :time_format must be one of [:short, :medium, :long] if :format is also one of [:short, :medium, :long]. Found :short and \"invalid\"."}} = 242 | MyApp.Cldr.DateTime.Interval.to_string( 243 | ~U[2020-01-01 00:00:00.0Z], 244 | ~U[2020-01-01 10:00:00.0Z], 245 | format: :medium, 246 | date_format: :short, 247 | time_format: "invalid" 248 | ) 249 | 250 | assert {:error, 251 | {Cldr.DateTime.InvalidFormat, 252 | ":date_format and :time_format must be one of [:short, :medium, :long] if :format is also one of [:short, :medium, :long]. Found \"invalid\" and :short."}} = 253 | MyApp.Cldr.DateTime.Interval.to_string( 254 | ~U[2020-01-01 00:00:00.0Z], 255 | ~U[2020-01-01 10:00:00.0Z], 256 | format: :medium, 257 | date_format: "invalid", 258 | time_format: :short 259 | ) 260 | end 261 | 262 | test "If :format is a string or atom (other than standard formats) then :date_format and :time_format are not permitted" do 263 | assert {:error, 264 | {Cldr.DateTime.InvalidFormat, 265 | ":date_format and :time_format cannot be specified when the interval format is a binary or atom other than one of [:short, :medium, :long]. Found: \"y-M-d - d\"."}} = 266 | MyApp.Cldr.DateTime.Interval.to_string( 267 | ~U[2020-01-01 00:00:00.0Z], 268 | ~U[2020-01-01 10:00:00.0Z], 269 | format: "y-M-d - d", 270 | date_format: :short, 271 | time_format: :long 272 | ) 273 | 274 | assert {:error, 275 | {Cldr.DateTime.InvalidFormat, 276 | ":date_format and :time_format cannot be specified when the interval format is a binary or atom other than one of [:short, :medium, :long]. Found: :gMY."}} = 277 | MyApp.Cldr.DateTime.Interval.to_string( 278 | ~U[2020-01-01 00:00:00.0Z], 279 | ~U[2020-01-01 10:00:00.0Z], 280 | format: :gMY, 281 | date_format: :short, 282 | time_format: :short 283 | ) 284 | end 285 | end 286 | -------------------------------------------------------------------------------- /test/partial_date_times_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Cldr.DateTime.PartialTest do 2 | use ExUnit.Case, async: true 3 | 4 | test "Partial Dates" do 5 | assert {:ok, "2024"} = Cldr.Date.to_string(%{year: 2024}) 6 | assert {:ok, "3/2024"} = Cldr.Date.to_string(%{year: 2024, month: 3}) 7 | assert {:ok, "3/5"} = Cldr.Date.to_string(%{month: 3, day: 5}) 8 | assert {:ok, "5-3"} = Cldr.Date.to_string(%{month: 3, day: 5}, format: "d-M") 9 | 10 | assert Cldr.Date.to_string(%{year: 3, day: 5}, format: "d-M") == 11 | {:error, 12 | {Cldr.DateTime.FormatError, 13 | "The format symbol 'M' requires at map with at least :month. " <> 14 | "Found: %{calendar: Cldr.Calendar.Gregorian, day: 5, year: 3}"}} 15 | 16 | assert Cldr.Date.to_string(%{year: 3, day: 5}) == 17 | {:error, {Cldr.DateTime.UnresolvedFormat, "No available format resolved for :dy"}} 18 | end 19 | 20 | test "Partial Times" do 21 | assert {:ok, "11 PM"} = Cldr.Time.to_string(%{hour: 23}) 22 | assert {:ok, "11:15 PM"} = Cldr.Time.to_string(%{hour: 23, minute: 15}) 23 | assert {:ok, "11:15:45 PM"} = Cldr.Time.to_string(%{hour: 23, minute: 15, second: 45}) 24 | assert {:ok, "23:45"} = Cldr.Time.to_string(%{minute: 23, second: 45}) 25 | 26 | assert {:ok, "5:23 AM Australia/Sydney"} = 27 | Cldr.Time.to_string(%{hour: 5, minute: 23, time_zone: "Australia/Sydney"}) 28 | 29 | assert {:ok, "5:23 unk"} = 30 | Cldr.Time.to_string(%{hour: 5, minute: 23, zone_abbr: "AEST"}, format: "h:m V") 31 | 32 | assert {:ok, "5:23 AEST"} = 33 | Cldr.Time.to_string(%{hour: 5, minute: 23, zone_abbr: "AEST"}, format: "h:m VV") 34 | 35 | assert {:ok, "5:23 GMT"} = 36 | Cldr.Time.to_string( 37 | %{hour: 5, minute: 23, zone_abbr: "AEST", utc_offset: 0, std_offset: 0}, 38 | format: "h:m VVVV" 39 | ) 40 | 41 | assert Cldr.Time.to_string(%{hour: 5, minute: 23, zone_abbr: "AEST"}, format: "h:m VVVV") 42 | 43 | {:error, 44 | {Cldr.DateTime.FormatError, 45 | "The format symbol 'x' requires at map with at least :utc_offset. " <> 46 | "Found: %{calendar: Cldr.Calendar.Gregorian, minute: 23, hour: 5, zone_abbr: \"AEST\"}"}} 47 | 48 | assert Cldr.Time.to_string(%{hour: 23, second: 45}) == 49 | {:error, {Cldr.DateTime.UnresolvedFormat, "No available format resolved for :sh"}} 50 | 51 | assert Cldr.Time.to_string(%{hour: 23, second: 45}, format: "h:m:s") == 52 | {:error, 53 | {Cldr.DateTime.FormatError, 54 | "The format symbol 'm' requires at map with at least :minute. " <> 55 | "Found: %{second: 45, calendar: Cldr.Calendar.Gregorian, hour: 23}"}} 56 | end 57 | 58 | test "Partial date times" do 59 | assert {:ok, "11/2024, 10 AM"} = Cldr.DateTime.to_string(%{year: 2024, month: 11, hour: 10}) 60 | assert {:ok, "2024, 10 AM"} = Cldr.DateTime.to_string(%{year: 2024, hour: 10}) 61 | 62 | assert Cldr.DateTime.to_string(%{year: 2024, minute: 10}) == 63 | {:error, {Cldr.DateTime.UnresolvedFormat, "No available format resolved for :m"}} 64 | end 65 | 66 | test "Additional error returns" do 67 | assert Cldr.DateTime.to_string(%{hour: 2024}) == 68 | {:error, {Cldr.DateTime.FormatError, "Hour must be in the range of 0..24. Found 2024"}} 69 | 70 | assert Cldr.Time.to_string(%{hour: 2024}) == 71 | {:error, {Cldr.DateTime.FormatError, "Hour must be in the range of 0..24. Found 2024"}} 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start(trace: "--trace" in System.argv(), timeout: 220_000) 2 | -------------------------------------------------------------------------------- /test/variant_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Cldr.DateTime.VariantTest do 2 | use ExUnit.Case, async: true 3 | 4 | test "Time Unicode or ASCII preference" do 5 | datetime = ~U[2024-07-07 21:36:00.440105Z] 6 | 7 | unicode = Cldr.DateTime.to_string(datetime, format: :Ehm, prefer: :unicode, locale: :en) 8 | ascii = Cldr.DateTime.to_string(datetime, format: :Ehm, prefer: :ascii, locale: :en) 9 | assert unicode != ascii 10 | end 11 | 12 | test "Time preference" do 13 | datetime = ~U[2024-07-07 21:36:00.440105Z] 14 | 15 | assert {:ok, "Sun 9:36 PM"} = 16 | Cldr.DateTime.to_string(datetime, format: :Ehm, prefer: :unicode, locale: :en) 17 | 18 | assert {:ok, "Sun 9:36 PM"} = 19 | Cldr.DateTime.to_string(datetime, format: :Ehm, prefer: :ascii, locale: :en) 20 | 21 | assert {:ok, "Sun 9:36 PM"} = 22 | Cldr.DateTime.to_string(datetime, format: :Ehm, prefer: [:unicode], locale: :en) 23 | 24 | assert {:ok, "Sun 9:36 PM"} = 25 | Cldr.DateTime.to_string(datetime, format: :Ehm, prefer: [:ascii], locale: :en) 26 | end 27 | 28 | test "Date interval variant" do 29 | assert {:ok, "1/1/2024 – 1/2/2024"} = 30 | Cldr.Date.Interval.to_string(~D[2024-01-01], ~D[2024-02-01], 31 | format: :yMd, 32 | prefer: :variant, 33 | locale: "en-CA" 34 | ) 35 | 36 | assert {:ok, "1/1/2024–2/1/2024"} = 37 | Cldr.Date.Interval.to_string(~D[2024-01-01], ~D[2024-02-01], 38 | format: :yMd, 39 | prefer: :default, 40 | locale: "en-CA" 41 | ) 42 | 43 | assert {:ok, "1/1/2024 – 1/2/2024"} = 44 | Cldr.Date.Interval.to_string(~D[2024-01-01], ~D[2024-02-01], 45 | format: :yMd, 46 | prefer: [:variant], 47 | locale: "en-CA" 48 | ) 49 | 50 | assert {:ok, "1/1/2024–2/1/2024"} = 51 | Cldr.Date.Interval.to_string(~D[2024-01-01], ~D[2024-02-01], 52 | format: :yMd, 53 | prefer: [:default], 54 | locale: "en-CA" 55 | ) 56 | 57 | assert {:ok, "1/1/2024–2/1/2024"} = 58 | Cldr.Date.Interval.to_string(~D[2024-01-01], ~D[2024-02-01], 59 | format: :yMd, 60 | locale: "en-CA" 61 | ) 62 | end 63 | 64 | test "Date variant" do 65 | assert {:ok, "1/3/2024"} = 66 | Cldr.Date.to_string(~D[2024-03-01], format: :yMd, prefer: :variant, locale: "en-CA") 67 | 68 | assert {:ok, "2024-03-01"} = 69 | Cldr.Date.to_string(~D[2024-03-01], format: :yMd, prefer: :default, locale: "en-CA") 70 | 71 | assert {:ok, "1/3/2024"} = 72 | Cldr.Date.to_string(~D[2024-03-01], format: :yMd, prefer: [:variant], locale: "en-CA") 73 | 74 | assert {:ok, "2024-03-01"} = 75 | Cldr.Date.to_string(~D[2024-03-01], format: :yMd, prefer: [:default], locale: "en-CA") 76 | 77 | assert {:ok, "2024-03-01"} = 78 | Cldr.Date.to_string(~D[2024-03-01], format: :yMd, locale: "en-CA") 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /test/wrapper_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Cldr.DateTime.WrapperTest do 2 | use ExUnit.Case, async: true 3 | 4 | test "wrapping a datetime" do 5 | wrapper = fn value, type -> 6 | ["<", to_string(type), ">", to_string(value), ""] 7 | end 8 | 9 | assert {:ok, 10 | "Mar 13" <> 11 | ", 2023, " <> 12 | "9:41" <> 13 | ":00" <> 14 | "PM"} = 15 | Cldr.DateTime.to_string(~U[2023-03-13T21:41:00.0Z], wrapper: wrapper) 16 | end 17 | end 18 | --------------------------------------------------------------------------------