Created
May 4, 2026 22:19
-
-
Save lagbox/a044866796e56557a07bea4e2d48fc74 to your computer and use it in GitHub Desktop.
utilities for rendering numbers and text using Unicode segmented digits
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| defmodule Core.Presentation.Numbers do | |
| @moduledoc """ | |
| Utilities for rendering numbers and text using Unicode segmented digits (🯰–🯹). | |
| Supports: | |
| - integers, floats, strings | |
| - display formatting (padding) | |
| - clock formatting mode | |
| """ | |
| @segmented_characters_base 0x1FBF0 | |
| @segment_minus "─" | |
| @segment_colon ":" | |
| # clock | |
| @formats %{ | |
| standard: %{parts: [:h, :m, :s], sep: ":"}, | |
| compact: %{parts: [:hm, :s], sep: ":"}, | |
| flat: %{parts: [:h, :m, :s], sep: ""}, | |
| hm: %{parts: [:h, :m], sep: ":"}, | |
| hm_flat: %{parts: [:hm], sep: ""} | |
| } | |
| # --------------------------- | |
| # Public API | |
| # --------------------------- | |
| @doc """ | |
| Converts input into segmented digit representation. | |
| Options: | |
| - :mode (:number | :clock | :display) | |
| - :width (integer padding width, only for :display or :number) | |
| - :decimals (float precision, default: 10) | |
| - :pad_char (default "0" for numbers, " " for display) | |
| - :format (:standard | :compact | :flat | :hm | :hm_flat, default: :standard, only for :clock) | |
| """ | |
| def to_digital(input, opts \\ []) | |
| def to_digital(number, opts) when is_integer(number) do | |
| mode = Keyword.get(opts, :mode, :number) | |
| width = Keyword.get(opts, :width) | |
| pad_char = Keyword.get(opts, :pad_char, if(mode == :display, do: " ", else: "0")) | |
| number | |
| |> Integer.to_string() | |
| |> maybe_pad(if(mode == :display, do: width), pad_char) | |
| |> render_binary() | |
| end | |
| # --------------------------- | |
| # Float path | |
| # --------------------------- | |
| def to_digital(float, opts) when is_float(float) do | |
| decimals = Keyword.get(opts, :decimals, 10) | |
| float | |
| |> :erlang.float_to_binary(decimals: decimals) | |
| # |> render_binary() | |
| |> to_digital(opts) | |
| end | |
| # --------------------------- | |
| # String path | |
| # --------------------------- | |
| def to_digital(binary, _opts) when is_binary(binary) do | |
| render_binary(binary) | |
| end | |
| # --------------------------- | |
| # List path (explicit, not enumerable) | |
| # --------------------------- | |
| def to_digital(list, opts) when is_list(list) do | |
| list | |
| |> to_string() | |
| |> to_digital(opts) | |
| end | |
| # --------------------------- | |
| # Convenience wrappers (optional API sugar) | |
| # --------------------------- | |
| def to_display(n, width) do | |
| to_digital(n, mode: :display, width: width) | |
| end | |
| def to_clock({h, m, s}, format \\ :standard) do | |
| time = {pad2(h), pad2(m), pad2(s)} | |
| %{parts: parts, sep: sep} = Map.fetch!(@formats, format) | |
| parts | |
| |> Enum.map_join(sep, &render_token(&1, time)) | |
| |> render_binary() | |
| end | |
| defp render_token(:h, {h, _, _}), do: h | |
| defp render_token(:m, {_, m, _}), do: m | |
| defp render_token(:s, {_, _, s}), do: s | |
| defp render_token(:hm, {h, m, _}), do: h <> m | |
| # defp render_token(key, {h, m, s}) do | |
| # case key do | |
| # :h -> h | |
| # :m -> m | |
| # :s -> s | |
| # :hm -> h <> m | |
| # end | |
| # end | |
| # --------------------------- | |
| # Core renderer | |
| # --------------------------- | |
| defp render_binary(binary) do | |
| for <<char::utf8 <- binary>>, into: "" do | |
| digitalize_char(char) | |
| end | |
| end | |
| # --------------------------- | |
| # Character mapping | |
| # --------------------------- | |
| defp digitalize_char(?-), do: @segment_minus | |
| defp digitalize_char(?:), do: @segment_colon | |
| defp digitalize_char(d) when d in ?0..?9, do: number_to_digital(d - ?0) | |
| defp digitalize_char(char), do: <<char::utf8>> | |
| defp number_to_digital(n) when n in 0..9 do | |
| <<@segmented_characters_base + n::utf8>> | |
| end | |
| # --------------------------- | |
| # Helpers | |
| # --------------------------- | |
| defp pad2(n), do: String.pad_leading(Integer.to_string(n), 2, "0") | |
| defp maybe_pad(str, nil, _pad_char), do: str | |
| defp maybe_pad("-" <> rest, width, pad_char) do | |
| "-" <> String.pad_leading(rest, width - 1, pad_char) | |
| end | |
| defp maybe_pad(str, width, pad_char) do | |
| String.pad_leading(str, width, pad_char) | |
| end | |
| end | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment