├── .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 | 
4 | [](https://hex.pm/packages/ex_cldr_dates_times)
5 | [](https://hex.pm/packages/ex_cldr_dates_times)
6 | [](https://hex.pm/packages/ex_cldr_dates_times)
7 | [](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), "", to_string(type), ">"]
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 |
--------------------------------------------------------------------------------