Skip to content

Commit 57ff91b

Browse files
authored
Merge branch 'main' into version-0.1.2
2 parents 2ba42ff + 1aafc6f commit 57ff91b

File tree

10 files changed

+446
-116
lines changed

10 files changed

+446
-116
lines changed

.github/workflows/test.yml

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
name: Tests
2+
3+
on:
4+
workflow_call:
5+
inputs:
6+
skip-check-formatted:
7+
type: boolean
8+
default: false
9+
push:
10+
branches:
11+
- "**"
12+
13+
env:
14+
MIX_ENV: test
15+
jobs:
16+
test:
17+
runs-on: ubuntu-latest
18+
name: Run Tests
19+
steps:
20+
- name: Git Clone the Repository
21+
uses: actions/checkout@v4
22+
23+
# See https://github.com/erlef/setup-beam/issues/325
24+
- name: Set ImageOS Env (fix for erlef/setup-beam)
25+
id: erlef-fix
26+
run: |
27+
VSN=$(cat /etc/os-release | grep VERSION_ID | awk -F '"' '{ print $2 }' | awk -F "." '{ print $1 }')
28+
echo "ImageOS=ubuntu${VSN}" >> $GITHUB_OUTPUT
29+
30+
- name: Set up Elixir
31+
uses: erlef/setup-beam@v1
32+
with:
33+
version-file: .tool-versions
34+
version-type: strict
35+
env:
36+
ImageOS: ${{ steps.erlef-fix.outputs.ImageOS }}
37+
38+
- name: Cache Mix Dependencies
39+
uses: actions/cache@v4
40+
env:
41+
cache-name: elixir-deps
42+
with:
43+
path: |
44+
deps
45+
_build
46+
key: ${{ runner.os }}-${{ runner.arch }}-${{ env.cache-name }}-${{ steps.setup-beam.outputs.elixir-version }}-${{ steps.setup-beam.outputs.otp-version }}-${{ hashFiles('**/mix.lock') }}
47+
restore-keys: |
48+
${{ runner.os }}-${{ runner.arch }}-${{ env.cache-name }}-${{ steps.setup-beam.outputs.elixir-version }}-${{ steps.setup-beam.outputs.otp-version }}-
49+
50+
- name: Fetch Mix Deps
51+
run: mix deps.get
52+
53+
- name: Compile Project
54+
run: mix compile --warnings-as-errors
55+
56+
- name: Run Tests
57+
run: mix test --trace
58+
59+
- name: Check Formatting
60+
run: mix format --check-formatted
61+
if: ${{ github.event.inputs.skip-check-formatted != true }}

.tool-versions

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
elixir 1.18.3-otp-27
2+
erlang 27.3.3

README.md

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,16 @@
11
# ValueFormatters
22

3-
**TODO: Add description**
3+
## Usage
44

5-
## Installation
5+
To use value formatters, the host application needs to set up its own Cldr backend, including the following libraries:
66

7-
If [available in Hex](https://hex.pm/docs/publish), the package can be installed
8-
by adding `value_formatters` to your list of dependencies in `mix.exs`:
7+
* Cldr.Number,
8+
* Cldr.Calendar,
9+
* Cldr.DateTime,
10+
* Cldr.Time,
11+
* Cldr.Date
12+
13+
Cldr needs to be passed as an option to the `to_string/3` method under `cldr` key.
914

10-
```elixir
11-
def deps do
12-
[
13-
{:value_formatters, "~> 0.1.0"}
14-
]
15-
end
16-
```
1715

18-
Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
19-
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
20-
be found at <https://hexdocs.pm/value_formatters>.
2116

lib/value_formatters.ex

Lines changed: 116 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,62 @@
11
defmodule ValueFormatters do
22
use OK.Pipe
3-
alias ValueFormatters.Cldr
3+
4+
# Allows instantiation of the module with preset options.
5+
defmacro __using__(module_opts) do
6+
quote do
7+
def to_string(value, format_definition, opts \\ []) do
8+
defaults =
9+
Keyword.get(unquote(module_opts), :defaults, %{})
10+
|> Map.merge(Keyword.get(opts, :defaults, %{}))
11+
12+
opts =
13+
Keyword.put_new(opts, :cldr, unquote(module_opts[:cldr]))
14+
|> Keyword.put(:defaults, defaults)
15+
16+
ValueFormatters.to_string(value, format_definition, opts)
17+
end
18+
end
19+
end
420

521
@date_display_options [:none, :short, :medium, :long, :full]
622
@time_display_options [:none, :short, :medium, :long, :full]
723

8-
def to_string(value, format_definition, options \\ [])
9-
10-
def to_string(nil, _format_definition, _options), do: {:ok, ""}
11-
def to_string("", _format_definition, _options), do: {:ok, ""}
24+
def to_string(value, format_definition, opts \\ [])
1225

13-
# def to_string(true, _format_definition, _options), do: {:ok, Cldr.Message.format("Yes")}
14-
# def to_string(false, _format_definition, _options), do: {:ok, Cldr.Message.format("No")}
26+
def to_string(nil, _format_definition, _opts), do: {:ok, ""}
27+
def to_string("", _format_definition, _opts), do: {:ok, ""}
1528

16-
def to_string(value, format_definition, options) do
29+
def to_string(value, format_definition, opts) do
1730
format_definition =
1831
format_definition
1932
|> expand_format_definition(value)
20-
|> merge_with_defaults(options)
33+
|> merge_with_defaults(opts)
2134

2235
# do the formatting
2336
case format_definition["format"] do
24-
"number" -> format_number(value, format_definition, options)
25-
"string" -> format_string(value, format_definition)
26-
"date" -> format_date(value, format_definition, options)
27-
"date_relative" -> format_date_relative(value, format_definition, options)
28-
"coordinates" -> format_coordinates(value, format_definition)
29-
_ -> {:error, "Unsupported format #{format_definition["format"]}"}
37+
"number" ->
38+
format_number(value, format_definition, opts)
39+
40+
"string" ->
41+
format_string(value, format_definition)
42+
43+
"date" ->
44+
format_date(value, format_definition, opts)
45+
46+
"date_relative" ->
47+
format_date_relative(value, format_definition, opts)
48+
49+
"date_iso" ->
50+
format_date_iso(value, format_definition, opts)
51+
52+
"date_unix" ->
53+
format_date_unix(value, format_definition, opts)
54+
55+
"coordinates" ->
56+
format_coordinates(value, format_definition, opts)
57+
58+
_ ->
59+
{:error, "Unsupported format #{format_definition["format"]}"}
3060
end
3161
|> handle_cldr_error()
3262
end
@@ -37,8 +67,7 @@ defmodule ValueFormatters do
3767
end
3868

3969
# In case of a shorthand formatDefinition, expand it
40-
defp expand_format_definition(format_definition, _value)
41-
when format_definition in ["number", "string", "date", "date_relative", "coordinates"] do
70+
defp expand_format_definition(format_definition, _value) when is_binary(format_definition) do
4271
%{"format" => format_definition}
4372
end
4473

@@ -82,6 +111,17 @@ defmodule ValueFormatters do
82111
end
83112
end
84113

114+
defp cldr(opts, mod_name) do
115+
with {:ok, cldr} <- Keyword.fetch(opts, :cldr) do
116+
Module.concat(cldr, mod_name)
117+
else
118+
:error ->
119+
raise ArgumentError,
120+
message:
121+
"Attempted to access a Cldr module, but non was specified. Pass the :cldr option to to_string/3 or when instantiating the module."
122+
end
123+
end
124+
85125
defp format_number(value, number_definition, opts) when is_number(value) do
86126
precision = Map.get(number_definition, "precision")
87127
unit = Map.get(number_definition, "unit")
@@ -97,7 +137,10 @@ defmodule ValueFormatters do
97137
{value, precision}
98138
end
99139

100-
Cldr.Number.to_string(rounded_value, locale: get_locale(opts), fractional_digits: precision)
140+
cldr(opts, Number).to_string(rounded_value,
141+
locale: get_locale(opts),
142+
fractional_digits: precision
143+
)
101144
~> append_unit(unit, opts)
102145
end
103146

@@ -155,22 +198,60 @@ defmodule ValueFormatters do
155198

156199
# Value of type Time has to be formatted with Cldr.Time
157200
date_display == :none or is_time(value) ->
158-
Cldr.Time.to_string(value, format: time_display, locale: get_locale(opts))
201+
cldr(opts, Time).to_string(value, format: time_display, locale: get_locale(opts))
159202

160203
# Value of type Date has to be formatted with Cldr.Date
161204
time_display == :none or is_date(value) ->
162-
Cldr.Date.to_string(value, format: date_display, locale: get_locale(opts))
205+
cldr(opts, Date).to_string(value, format: date_display, locale: get_locale(opts))
163206

164207
# Covers DateTime and NaiveDateTime
165208
true ->
166-
Cldr.DateTime.to_string(value,
209+
cldr(opts, DateTime).to_string(value,
167210
date_format: date_display,
168211
time_format: time_display,
169212
locale: get_locale(opts)
170213
)
171214
end
172-
else
173-
{:error, reason} -> {:error, reason}
215+
end
216+
end
217+
218+
defp format_date_iso(value, date_definition, opts) do
219+
with {:ok, value} <- pre_process_date_value(value, date_definition, opts) do
220+
case value do
221+
%Date{} ->
222+
{:ok, Date.to_iso8601(value)}
223+
224+
%Time{} ->
225+
{:ok, Time.to_iso8601(value)}
226+
227+
%DateTime{} ->
228+
{:ok, DateTime.to_iso8601(value)}
229+
230+
_ ->
231+
{:error, "Invalid Date/Time value #{value}"}
232+
end
233+
end
234+
end
235+
236+
defp format_date_unix(value, date_definition, opts) do
237+
with {:ok, value} <- pre_process_date_value(value, date_definition, opts),
238+
{:ok, value} <- ensure_unix_convertible(value) do
239+
milliseconds =
240+
Map.get(date_definition, "milliseconds", false)
241+
242+
precision = if milliseconds, do: :millisecond, else: :second
243+
244+
DateTime.to_unix(value, precision)
245+
|> Integer.to_string()
246+
|> OK.wrap()
247+
end
248+
end
249+
250+
defp ensure_unix_convertible(value) do
251+
case value do
252+
%Date{} -> DateTime.new(value, ~T[00:00:00])
253+
%DateTime{} -> {:ok, value}
254+
_ -> {:error, "Value #{inspect(value)} is not a Date or DateTime."}
174255
end
175256
end
176257

@@ -242,7 +323,7 @@ defmodule ValueFormatters do
242323
defp format_date_relative(value, date_definition, opts) do
243324
with {:ok, value} <- pre_process_date_value(value, date_definition, opts) do
244325
if not is_time(value) do
245-
Cldr.DateTime.Relative.to_string(value, locale: get_locale(opts))
326+
cldr(opts, DateTime.Relative).to_string(value, locale: get_locale(opts))
246327
else
247328
{:error, "Date part is required for relative date formatting."}
248329
end
@@ -251,7 +332,7 @@ defmodule ValueFormatters do
251332
end
252333
end
253334

254-
defp format_coordinates(value, coordinate_definition) do
335+
defp format_coordinates(value, coordinate_definition, opts) do
255336
[lat, lng, radius] =
256337
case value do
257338
%{"lat" => lat, "lng" => lng, "radius" => radius} -> [lat, lng, radius]
@@ -261,11 +342,16 @@ defmodule ValueFormatters do
261342
end
262343

263344
with {:ok, lat_formatted} <-
264-
format_number(lat, %{"format" => "number", "precision" => 5}, []),
265-
{:ok, lng_formatted} <- format_number(lng, %{"format" => "number", "precision" => 5}, []) do
345+
format_number(lat, %{"format" => "number", "precision" => 5}, opts),
346+
{:ok, lng_formatted} <-
347+
format_number(lng, %{"format" => "number", "precision" => 5}, opts) do
266348
if get_in(coordinate_definition, ["radius_display"]) != false and radius != nil do
267349
with {:ok, radius_formatted} <-
268-
format_number(radius, %{"format" => "number", "precision" => 0, "unit" => "m"}, []) do
350+
format_number(
351+
radius,
352+
%{"format" => "number", "precision" => 0, "unit" => "m"},
353+
opts
354+
) do
269355
{:ok, "#{lat_formatted}\u{00B0}, #{lng_formatted}\u{00B0}, #{radius_formatted}"}
270356
else
271357
{:error, reason} -> {:error, reason}
@@ -278,10 +364,9 @@ defmodule ValueFormatters do
278364
end
279365
end
280366

281-
defp get_locale(opts, default \\ nil) do
367+
defp get_locale(opts) do
282368
Keyword.get(opts, :locale) ||
283-
Process.get(:locale) ||
284-
default
369+
Process.get(:locale)
285370
end
286371

287372
defp get_timezone(opts, default \\ nil) do

mix.exs

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,12 @@ defmodule ValueFormatters.MixProject do
66
app: :value_formatters,
77
version: "0.1.2",
88
elixir: "~> 1.15",
9+
elixirc_paths: elixirc_paths(Mix.env()),
910
start_permanent: Mix.env() == :prod,
10-
deps: deps()
11+
deps: deps(),
12+
preferred_cli_env: [
13+
"test.watch": :test
14+
]
1115
]
1216
end
1317

@@ -22,11 +26,16 @@ defmodule ValueFormatters.MixProject do
2226
defp deps do
2327
[
2428
{:ex_doc, "~> 0.38.2", only: :dev},
25-
{:ex_cldr_dates_times, "~> 2.22"},
26-
{:ex_cldr_lists, "~> 2.10"},
27-
{:ex_cldr_calendars, "~> 2.1"},
29+
{:ex_cldr_dates_times, "~> 2.22", only: [:dev, :test]},
30+
{:ex_cldr_lists, "~> 2.10", only: [:dev, :test]},
31+
{:ex_cldr_calendars, "~> 2.1", only: [:dev, :test]},
32+
{:mox, "~> 1.0", only: [:dev, :test]},
2833
{:timex, "~> 3.7", only: :test},
29-
{:ok, "~> 2.3.0"}
34+
{:ok, "~> 2.3.0"},
35+
{:mix_test_watch, "~> 1.3", only: [:dev, :test]}
3036
]
3137
end
38+
39+
defp elixirc_paths(:test), do: ["lib", "test"]
40+
defp elixirc_paths(_), do: ["lib"]
3241
end

mix.lock

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"ex_cldr_numbers": {:hex, :ex_cldr_numbers, "2.35.1", "aa84601e604f6656b4bfb7f58a329328d2cbc13e4601da9071c14af38fc1d641", [: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.42", [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", "925c25debb0a4d27b3559885a79fb66dd4ebb6b72ce46b1e971db647042e28a0"},
1515
"ex_doc": {:hex, :ex_doc, "0.38.2", "504d25eef296b4dec3b8e33e810bc8b5344d565998cd83914ffe1b8503737c02", [:mix], [{:earmark_parser, "~> 1.4.44", [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", "732f2d972e42c116a70802f9898c51b54916e542cc50968ac6980512ec90f42b"},
1616
"expo": {:hex, :expo, "1.1.0", "f7b9ed7fb5745ebe1eeedf3d6f29226c5dd52897ac67c0f8af62a07e661e5c75", [:mix], [], "hexpm", "fbadf93f4700fb44c331362177bdca9eeb8097e8b0ef525c9cc501cb9917c960"},
17+
"file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"},
1718
"gettext": {:hex, :gettext, "0.26.2", "5978aa7b21fada6deabf1f6341ddba50bc69c999e812211903b169799208f2a8", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "aa978504bcf76511efdc22d580ba08e2279caab1066b76bb9aa81c4a1e0a32a5"},
1819
"hackney": {:hex, :hackney, "1.24.1", "f5205a125bba6ed4587f9db3cc7c729d11316fa8f215d3e57ed1c067a9703fa9", [:rebar3], [{:certifi, "~> 2.15.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.4", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "f4a7392a0b53d8bbc3eb855bdcc919cd677358e65b2afd3840b5b3690c4c8a39"},
1920
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
@@ -23,6 +24,9 @@
2324
"makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"},
2425
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
2526
"mimerl": {:hex, :mimerl, "1.4.0", "3882a5ca67fbbe7117ba8947f27643557adec38fa2307490c4c4207624cb213b", [:rebar3], [], "hexpm", "13af15f9f68c65884ecca3a3891d50a7b57d82152792f3e19d88650aa126b144"},
27+
"mix_test_watch": {:hex, :mix_test_watch, "1.3.0", "2ffc9f72b0d1f4ecf0ce97b044e0e3c607c3b4dc21d6228365e8bc7c2856dc77", [:mix], [{:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "f9e5edca976857ffac78632e635750d158df14ee2d6185a15013844af7570ffe"},
28+
"mox": {:hex, :mox, "1.2.0", "a2cd96b4b80a3883e3100a221e8adc1b98e4c3a332a8fc434c39526babafd5b3", [:mix], [{:nimble_ownership, "~> 1.0", [hex: :nimble_ownership, repo: "hexpm", optional: false]}], "hexpm", "c7b92b3cc69ee24a7eeeaf944cd7be22013c52fcb580c1f33f50845ec821089a"},
29+
"nimble_ownership": {:hex, :nimble_ownership, "1.0.1", "f69fae0cdd451b1614364013544e66e4f5d25f36a2056a9698b793305c5aa3a6", [:mix], [], "hexpm", "3825e461025464f519f3f3e4a1f9b68c47dc151369611629ad08b636b73bb22d"},
2630
"nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"},
2731
"ok": {:hex, :ok, "2.3.0", "0a3d513ec9038504dc5359d44e14fc14ef59179e625563a1a144199cdc3a6d30", [:mix], [], "hexpm", "f0347b3f8f115bf347c704184b33cf084f2943771273f2b98a3707a5fa43c4d5"},
2832
"parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"},

0 commit comments

Comments
 (0)