Skip to content

Commit 4be3141

Browse files
committed
Release 1.0.0: require localize 1.0, add credo and strict dialyzer
1 parent 2591ad6 commit 4be3141

18 files changed

Lines changed: 229 additions & 150 deletions

.credo.exs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Credo configuration for Unity.
2+
#
3+
# Mirrors the Localize policy: strict, with `Design.AliasUsage` disabled.
4+
# Unit parsing and conversion code fully qualifies many calls because
5+
# module names such as `Localize.Unit` and `Unity.Unit` read more clearly
6+
# at the call site than an alias, and because trailing segments such as
7+
# `Unit`, `Parser` and `List` shadow other modules when aliased. Alias
8+
# submodules opportunistically where the trailing segment does not clash,
9+
# never as a bulk conversion.
10+
%{
11+
configs: [
12+
%{
13+
name: "default",
14+
strict: true,
15+
files: %{
16+
included: ["lib/", "test/"]
17+
},
18+
checks: %{
19+
disabled: [
20+
{Credo.Check.Design.AliasUsage, []}
21+
]
22+
}
23+
}
24+
]
25+
}

.github/workflows/ci.yml

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,12 @@ jobs:
4343
otp: "28"
4444

4545
# Elixir 1.20 — supports OTP 27–28
46-
- elixir: "1.20.0-rc.4-otp-27"
46+
- elixir: "1.20.2-otp-27"
4747
otp: "27"
48-
- elixir: "1.20.0-rc.4-otp-28"
48+
- elixir: "1.20.2-otp-28"
4949
otp: "28"
50+
- elixir: "1.20.2-otp-29"
51+
otp: "29"
5052
lint: true
5153

5254
steps:
@@ -76,6 +78,10 @@ jobs:
7678
if: matrix.lint
7779
run: mix format --check-formatted
7880

81+
- name: Run credo
82+
if: matrix.lint
83+
run: mix credo --strict
84+
7985
- name: Compile (warnings as errors)
8086
run: mix compile --warnings-as-errors
8187

@@ -102,3 +108,9 @@ jobs:
102108
env:
103109
MIX_ENV: dev
104110
run: mix dialyzer
111+
112+
- name: Check docs
113+
if: matrix.lint
114+
env:
115+
MIX_ENV: release
116+
run: mix docs --warnings-as-errors

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## v1.0.0 (2026-07-31)
4+
5+
### Changed
6+
7+
* Requires `localize ~> 1.0`. The previous requirement of `~> 0.9` excluded Localize 1.0.
8+
39
## v0.7.0 (2026-05-10)
410

511
### Enhancements

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ Add `unity` to your list of dependencies in `mix.exs`:
3434
```elixir
3535
def deps do
3636
[
37-
{:unity, "~> 0.5"}
37+
{:unity, "~> 1.0"}
3838
]
3939
end
4040
```

lib/unity/cli.ex

Lines changed: 80 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -31,44 +31,51 @@ defmodule Unity.CLI do
3131
"""
3232
@spec main([String.t()]) :: :ok
3333
def main(args) do
34-
{options, positional, _invalid} =
35-
OptionParser.parse(args,
36-
aliases: [
37-
v: :verbose,
38-
t: :terse,
39-
q: :quiet,
40-
h: :help,
41-
d: :digits,
42-
e: :exponential,
43-
o: :output_format,
44-
s: :strict,
45-
f: :file
46-
],
47-
switches: [
48-
verbose: :boolean,
49-
terse: :boolean,
50-
quiet: :boolean,
51-
strict: :boolean,
52-
exponential: :boolean,
53-
one_line: :boolean,
54-
no_color: :boolean,
55-
locale: :string,
56-
digits: :integer,
57-
output_format: :string,
58-
file: :keep,
59-
conformable: :string,
60-
list: :string,
61-
version: :boolean,
62-
help: :boolean
63-
]
64-
)
34+
{options, positional, _invalid} = parse_args(args)
6535

6636
# Load custom unit files before any evaluation
6737
load_custom_unit_files(options)
6838

6939
# Configure color output
7040
if options[:no_color], do: Application.put_env(:unity, :no_color, true)
7141

42+
dispatch(options, positional)
43+
end
44+
45+
defp parse_args(args) do
46+
OptionParser.parse(args,
47+
aliases: [
48+
v: :verbose,
49+
t: :terse,
50+
q: :quiet,
51+
h: :help,
52+
d: :digits,
53+
e: :exponential,
54+
o: :output_format,
55+
s: :strict,
56+
f: :file
57+
],
58+
switches: [
59+
verbose: :boolean,
60+
terse: :boolean,
61+
quiet: :boolean,
62+
strict: :boolean,
63+
exponential: :boolean,
64+
one_line: :boolean,
65+
no_color: :boolean,
66+
locale: :string,
67+
digits: :integer,
68+
output_format: :string,
69+
file: :keep,
70+
conformable: :string,
71+
list: :string,
72+
version: :boolean,
73+
help: :boolean
74+
]
75+
)
76+
end
77+
78+
defp dispatch(options, positional) do
7279
cond do
7380
options[:version] ->
7481
IO.puts("Unity v#{@version}")
@@ -102,24 +109,27 @@ defmodule Unity.CLI do
102109
format_options = build_format_options(options, nil)
103110

104111
IO.stream(:stdio, :line)
105-
|> Enum.each(fn line ->
106-
expression = String.trim(line)
112+
|> Enum.each(&evaluate_stdin_line(&1, format_options))
113+
end
107114

108-
if expression != "" do
109-
expr_options = Keyword.put(format_options, :input, expression)
115+
defp evaluate_stdin_line(line, format_options) do
116+
expression = String.trim(line)
110117

111-
case Unity.eval(expression) do
112-
{:ok, result, _env} ->
113-
case Unity.Formatter.format(result, expr_options) do
114-
{:ok, formatted} -> IO.puts(formatted)
115-
{:error, reason} -> IO.puts(:stderr, Unity.Error.format(reason))
116-
end
118+
if expression != "" do
119+
expr_options = Keyword.put(format_options, :input, expression)
117120

118-
{:error, message} ->
119-
IO.puts(:stderr, Unity.Error.format(message))
120-
end
121+
case Unity.eval(expression) do
122+
{:ok, result, _env} -> print_result(result, expr_options)
123+
{:error, message} -> IO.puts(:stderr, Unity.Error.format(message))
121124
end
122-
end)
125+
end
126+
end
127+
128+
defp print_result(result, expr_options) do
129+
case Unity.Formatter.format(result, expr_options) do
130+
{:ok, formatted} -> IO.puts(formatted)
131+
{:error, reason} -> IO.puts(:stderr, Unity.Error.format(reason))
132+
end
123133
end
124134

125135
defp io_tty? do
@@ -134,34 +144,37 @@ defmodule Unity.CLI do
134144
Localize.put_locale(locale)
135145
end
136146

137-
expression =
138-
case positional do
139-
# "units - feet" reads from stdin for the source
140-
["-", target] ->
141-
source = IO.read(:stdio, :eof) |> String.trim()
142-
"#{source} to #{target}"
147+
expression = expression_from(positional)
148+
format_options = build_format_options(options, expression)
143149

144-
[expr] ->
145-
expr
150+
case Unity.eval(expression) do
151+
{:ok, result, _env} -> print_result_or_exit(result, format_options)
152+
{:error, message} -> error_exit(message)
153+
end
154+
end
146155

147-
[from, to] ->
148-
"#{from} to #{to}"
156+
defp expression_from(positional) do
157+
case positional do
158+
# "units - feet" reads from stdin for the source
159+
["-", target] ->
160+
source = IO.read(:stdio, :eof) |> String.trim()
161+
"#{source} to #{target}"
149162

150-
_ ->
151-
Enum.join(positional, " ")
152-
end
163+
[expr] ->
164+
expr
153165

154-
format_options = build_format_options(options, expression)
166+
[from, to] ->
167+
"#{from} to #{to}"
155168

156-
case Unity.eval(expression) do
157-
{:ok, result, _env} ->
158-
case Unity.Formatter.format(result, format_options) do
159-
{:ok, formatted} -> IO.puts(formatted)
160-
{:error, reason} -> error_exit(reason)
161-
end
169+
_ ->
170+
Enum.join(positional, " ")
171+
end
172+
end
162173

163-
{:error, message} ->
164-
error_exit(message)
174+
defp print_result_or_exit(result, format_options) do
175+
case Unity.Formatter.format(result, format_options) do
176+
{:ok, formatted} -> IO.puts(formatted)
177+
{:error, reason} -> error_exit(reason)
165178
end
166179
end
167180

lib/unity/conversion/gauge.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ defmodule Unity.Conversion.Gauge do
3434
# Volume of sphere = 4/3 π r³, so r = (3V / 4π)^(1/3)
3535
# V = pound / (ga * leaddensity)
3636
@pound 0.45359237
37-
@lead_density 11340.0
37+
@lead_density 11_340.0
3838
@pi :math.pi()
3939

4040
@doc "Shotgun gauge number to bore diameter in meters."

lib/unity/gnu_units_importer.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ defmodule Unity.GnuUnitsImporter do
2323
2424
"""
2525

26-
alias Unity.GnuUnitsImporter.{Parser, Resolver, Registrar}
26+
alias Unity.GnuUnitsImporter.{Parser, Registrar, Resolver}
2727

2828
@bundled_path Application.app_dir(:unity, "priv/definitions.units")
2929

lib/unity/gnu_units_importer/parser.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ defmodule Unity.GnuUnitsImporter.Parser do
154154
%{state | primitives: Map.put(state.primitives, name, type)}
155155

156156
# Single-word expression that doesn't contain numbers or operators → alias
157-
is_alias?(expression) ->
157+
alias_expression?(expression) ->
158158
%{state | aliases: Map.put(state.aliases, name, expression)}
159159

160160
# Regular unit definition
@@ -163,7 +163,7 @@ defmodule Unity.GnuUnitsImporter.Parser do
163163
end
164164
end
165165

166-
defp is_alias?(expression) do
166+
defp alias_expression?(expression) do
167167
# An alias is a single identifier with no numbers, operators, spaces, or hyphens.
168168
# Must be purely alphabetic (no digits — "1e3" or "2^10" are not aliases).
169169
Regex.match?(~r/^[a-zA-Z_]+$/, expression)

lib/unity/gnu_units_importer/resolver.ex

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -197,13 +197,7 @@ defmodule Unity.GnuUnitsImporter.Resolver do
197197
sorted_prefixes = Process.get(:gnu_sorted_prefixes)
198198
known_bases = Process.get(:gnu_known_bases)
199199

200-
result =
201-
Enum.find_value(sorted_prefixes, fn prefix ->
202-
if String.starts_with?(name, prefix) and byte_size(name) > byte_size(prefix) do
203-
remainder = binary_part(name, byte_size(prefix), byte_size(name) - byte_size(prefix))
204-
if MapSet.member?(known_bases, remainder), do: {prefix, remainder}
205-
end
206-
end)
200+
result = Enum.find_value(sorted_prefixes, &split_known_prefix(name, &1, known_bases))
207201

208202
case result do
209203
{prefix, remainder} ->
@@ -219,6 +213,15 @@ defmodule Unity.GnuUnitsImporter.Resolver do
219213
end
220214
end
221215

216+
# Returns `{prefix, remainder}` when `name` starts with `prefix` and the
217+
# remainder is a known base unit, otherwise `nil` so `find_value` continues.
218+
defp split_known_prefix(name, prefix, known_bases) do
219+
if String.starts_with?(name, prefix) and byte_size(name) > byte_size(prefix) do
220+
remainder = binary_part(name, byte_size(prefix), byte_size(name) - byte_size(prefix))
221+
if MapSet.member?(known_bases, remainder), do: {prefix, remainder}
222+
end
223+
end
224+
222225
# ── Helpers ──
223226

224227
defp split_on_slash(tokens), do: do_split_slash(tokens, [], 0)

0 commit comments

Comments
 (0)