Skip to content

Instantly share code, notes, and snippets.

@lagbox
Created May 4, 2026 22:19
Show Gist options
  • Select an option

  • Save lagbox/a044866796e56557a07bea4e2d48fc74 to your computer and use it in GitHub Desktop.

Select an option

Save lagbox/a044866796e56557a07bea4e2d48fc74 to your computer and use it in GitHub Desktop.
utilities for rendering numbers and text using Unicode segmented digits
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