Compare commits

..

5 Commits

Author SHA1 Message Date
11730de24a WIP 2025-12-05 17:38:57 +09:00
975325db14 WIP 2025-12-02 19:40:33 +09:00
beed46a38b WIP: omg stuff 2025-12-02 15:22:13 +09:00
e4e6e35bf8 WIP: Fix remaining cases 2025-12-01 18:36:50 +09:00
7393a02de2 WIP: Moving formatters to HZ 2025-12-01 12:06:05 +09:00

549
src/hz_format.erl Normal file
View File

@ -0,0 +1,549 @@
%%% @doc
%%% GajuDesk Helper Functions
%%% @end
-module(hz_format).
-export([price/1, price/2, price/3, price/4,
read/1, read/2,
price_to_string/1, string_to_price/1]).
-spec price(Pucks) -> Formatted
when Pucks :: integer(),
Formatted :: string().
%% @doc
%% A convenience formatting function.
%% @equiv price(us, Pucks).
price(Pucks) ->
price(us, Pucks).
-spec price(Style, Pucks) -> Formatted
when Style :: us | ch | jp | {Separator, Myriad},
Separator :: $, | $. | $_,
Myriad :: 3 | 4,
Pucks :: integer(),
Formatted :: string().
%% @doc
%% A convenient formatting function.
%% @equiv price(gaju, Style, Pucks).
price(Style, Pucks) ->
price(gaju, Style, Pucks).
-spec price(Unit, Style, Pucks) -> Formatted
when Unit :: gaju | puck,
Style :: us | ch | jp | {Separator, Myriad},
Separator :: $, | $. | $_,
Myriad :: 3 | 4,
Pucks :: integer(),
Formatted :: string().
%% @doc
%% A simplified format function covering the most common formats desired.
price(gaju, us, Pucks) ->
western($,, $., 3, all, Pucks);
price(puck, us, Pucks) ->
western($,, 3, Pucks);
price(gaju, ch, Pucks) ->
western($., $,, 3, all, Pucks);
price(puck, ch, Pucks) ->
western($,, 3, Pucks);
price(Unit, jp, Pucks) ->
jp(Unit, all, Pucks);
price(gaju, {$., Myriad}, Pucks) ->
western($., $,, Myriad, all, Pucks);
price(gaju, {Separator, Myriad}, Pucks) ->
western(Separator, $., Myriad, all, Pucks);
price(puck, {Separator, Myriad}, Pucks) ->
western(Separator, Myriad, Pucks).
-spec price(Unit, Style, Precision, Pucks) -> Serialized
when Unit :: gaju | puck,
Style :: us | ch | jp | {Separator, Myriad},
Precision :: all | 0..18,
Separator :: $, | $. | $_,
Myriad :: 3 | 4,
Pucks :: integer(),
Serialized :: string().
%% @doc
%% A flexible, if annoyingly complex, formatting function.
%%
%% ```
%% price(gaju, us, 3, 123456789123456789123456789) ->
%% "木123,456,789.123...".
%%
%% price(gaju, ch, 3, 123456789123456789123456789) ->
%% "木123.456.789,123...".
%%
%% price(gaju, us, 3, 123456789123000000000000000) ->
%% "木123,456,789.123".
%%
%% price(gaju, {$,, 3}, 3, 123456789123456789123456789) ->
%% "木123,456,789.123...".
%%
%% price(gaju, {$,, 3}, 6, 123456789123000000000000000) ->
%% "木123,456,789.123"
%%
%% price(gaju, {$., 3}, all, 123456789123456789123456789) ->
%% "木123.456.789,123.456.789.123.456.789"
%%
%% price(gaju, {$_, 4}, 10, 123456789123456789123456789) ->
%% "木1_2345_6789.1234_5678_91..."
%%
%% price(gaju, jp, 3, 123456789123456789123456789) ->
%% "1億2345万6789木 12京3000兆本"
%%
%% price(gaju, jp, 6, 123456789123456789123456789) ->
%% "1億2345万6789木 12京3456兆本"
%%
%% price(gaju, jp, 0, 123456789123456789123456789) ->
%% "1億2345万6789木"
%%
%% price(puck, jp, all, 123456789123456789123456789) ->
%% "123秭4567垓8912京3456兆7891億2345万6789本"
%% '''
price(gaju, us, Precision, Pucks) ->
western($,, $., 3, Precision, Pucks);
price(gaju, ch, Precision, Pucks) ->
western($., $,, 3, Precision, Pucks);
price(gaju, jp, Precision, Pucks) ->
jp(gaju, Precision, Pucks);
price(gaju, {$., Myriad}, Precision, Pucks) ->
western($., $,, Myriad, Precision, Pucks);
price(gaju, {Separator, Myriad}, Precision, Pucks) ->
western(Separator, $., Myriad, Precision, Pucks);
price(puck, us, _, Pucks) ->
western($,, 3, Pucks);
price(puck, ch, _, Pucks) ->
western($., 3, Pucks);
price(puck, jp, _, Pucks) ->
jp(puck, all, Pucks);
price(puck, {Separator, Myriad}, _, Pucks) ->
western(Separator, Myriad, Pucks).
western(Separator, Myriad, Pucks) when Pucks >= 0 ->
western2(Separator, Myriad, Pucks);
western(Separator, Myriad, Pucks) when Pucks < 0 ->
[$- | western2(Separator, Myriad, Pucks * -1)].
western2(Separator, Myriad, Pucks) ->
P = lists:reverse(integer_to_list(Pucks)),
[puck_mark() | separate(Separator, Myriad, P)].
western(Separator, Break, Myriad, Precision, Pucks) when Pucks >= 0 ->
western2(Separator, Break, Myriad, Precision, Pucks);
western(Separator, Break, Myriad, Precision, Pucks) when Pucks < 0 ->
[$- | western2(Separator, Break, Myriad, Precision, Pucks)].
western2(Separator, _, Myriad, 0, Pucks) ->
G = lists:reverse(integer_to_list(Pucks div one_gaju())),
[gaju_mark() | separate(Separator, Myriad, G)];
western2(Separator, Break, Myriad, Precision, Pucks) ->
SP = integer_to_list(Pucks),
Length = length(SP),
Over18 = Length > 18,
NoPucks = (Pucks rem one_gaju()) =:= 0,
case {Over18, NoPucks} of
{true, true} ->
Gs = lists:reverse(lists:sublist(SP, Length - 18)),
[gaju_mark() | separate(Separator, Myriad, Gs)];
{true, false} ->
{PChars, GChars} = lists:split(18, lists:reverse(SP)),
H = [gaju_mark() | separate(Separator, Myriad, GChars)],
{P, E} = decimal_pucks(Precision, lists:reverse(PChars)),
T = lists:reverse(separate(Separator, Myriad, P)),
lists:flatten([H, Break, T, E]);
{false, true} ->
[gaju_mark(), $0];
{false, false} ->
PChars = lists:flatten(string:pad(SP, 18, leading, $0)),
{P, E} = decimal_pucks(Precision, PChars),
T = lists:reverse(separate(Separator, Myriad, P)),
lists:flatten([gaju_mark(), $0, Break, T, E])
end.
decimal_pucks(all, PChars) ->
io:format("decimal_pucks: PChars: ~p~n", [PChars]),
RTrailing = lists:reverse(PChars),
{lists:reverse(lists:dropwhile(fun(C) -> C =:= $0 end, RTrailing)), ""};
decimal_pucks(Precision, PChars) ->
{Significant, Rest} = lists:split(min(Precision, 18), PChars),
RTrailing = lists:reverse(Significant),
Trailing = lists:reverse(lists:dropwhile(fun(C) -> C =:= $0 end, RTrailing)),
case lists:all(fun(C) -> C =:= $0 end, Rest) of
true -> {Trailing, ""};
false -> {Trailing, "..."}
end.
separate(_, _, "") ->
"";
separate(S, P, G) ->
separate(S, P, 1, G, []).
separate(_, _, _, [H], A) ->
[H | A];
separate(S, P, P, [H | T], A) ->
separate(S, P, 1, T, [S, H | A]);
separate(S, P, N, [H | T], A) ->
separate(S, P, N + 1, T, [H | A]).
jp(Unit, Precision, Pucks) when Pucks >= 0 ->
jp2(Unit, Precision, Pucks);
jp(Unit, Precision, Pucks) when Pucks < 0 ->
[$, Formatted = jp2(Unit, Precision, Pucks * -1)].
jp2(gaju, 0, Pucks) ->
G = lists:reverse(integer_to_list(Pucks div one_gaju())),
myriad4(gaju_mark(), h, G);
jp2(gaju, all, Pucks) ->
H = jp(gaju, 0, Pucks),
P = lists:flatten(string:pad(integer_to_list(Pucks rem one_gaju()), 18, leading, $0)),
T = myriad4("", l, lists:reverse(P)),
lists:flatten([H, " ", T]);
jp2(gaju, Precision, Pucks) ->
H = jp(gaju, 0, Pucks),
P = lists:flatten(string:pad(integer_to_list(Pucks rem one_gaju()), 18, leading, $0)),
Digits = min(Precision, 18),
T =
case length(P) > Digits of
true ->
[];
false ->
ReverseP = lists:reverse(lists:sublist(P, Digits)),
PuckingString = lists:flatten(string:pad(ReverseP, 18, leading, $0)),
case lists:all(fun(C) -> C =:= $0 end, PuckingString) of
false -> myriad4(puck_mark(), l, PuckingString);
true -> ""
end
end,
T = myriad4("", l, lists:reverse(P)),
lists:flatten([H, " ", T]);
jp2(puck, all, Pucks) ->
P = lists:reverse(integer_to_list(Pucks)),
case lists:all(fun(C) -> C =:= $0 end, P) of
false -> myriad4(puck_mark(), h, P);
true -> [$0, puck_mark()]
end;
jp2(puck, Precision, Pucks) ->
Digits = min(Precision, 18),
P = lists:flatten(string:pad(integer_to_list(Pucks), 18, leading, $0)),
case length(P) < Digits of
true ->
[$0, puck_mark()];
false ->
PucksToGive = lists:sublist(P, Digits),
PuckingString = lists:flatten(string:pad(lists:reverse(PucksToGive), 18, leading, $0)),
case lists:all(fun(C) -> C =:= $0 end, PuckingString) of
false -> myriad4(puck_mark(), h, PuckingString);
true -> [$0, puck_mark()]
end
end.
myriad4(Symbol, Segment, [$0, $0, $0, $0 | PT]) ->
rank4(ranks(Segment), PT, [Symbol]);
myriad4(Symbol, Segment, [P4, $0, $0, $0 | PT]) ->
rank4(ranks(Segment), PT, [P4, Symbol]);
myriad4(Symbol, Segment, [P4, P3, $0, $0 | PT]) ->
rank4(ranks(Segment), PT, [P3, P4, Symbol]);
myriad4(Symbol, Segment, [P4, P3, P2, $0 | PT]) ->
rank4(ranks(Segment), PT, [P2, P3, P4, Symbol]);
myriad4(Symbol, Segment, [P4, P3, P2, P1 | PT]) ->
rank4(ranks(Segment), PT, [P1, P2, P3, P4, Symbol]);
myriad4(Symbol, _, [P4]) ->
[P4, Symbol];
myriad4(Symbol, _, [P4, P3]) ->
[P3, P4, Symbol];
myriad4(Symbol, _, [P4, P3, P2]) ->
[P2, P3, P4, Symbol].
rank4([_ | RT], [$0, $0, $0, $0 | PT], A) ->
rank4(RT, PT, A);
rank4([RH | RT], [P4, $0, $0, $0 | PT], A) ->
rank4(RT, PT, [P4, RH | A]);
rank4([RH | RT], [P4, P3, $0, $0 | PT], A) ->
rank4(RT, PT, [P3, P4, RH | A]);
rank4([RH | RT], [P4, P3, P2, $0 | PT], A) ->
rank4(RT, PT, [P2, P3, P4, RH | A]);
rank4([RH | RT], [P4, P3, P2, P1 | PT], A) ->
rank4(RT, PT, [P1, P2, P3, P4, RH | A]);
rank4(_, [$0, $0, $0, $0], A) ->
A;
rank4(_, [$0, $0, $0], A) ->
A;
rank4(_, [$0, $0], A) ->
A;
rank4(_, [$0], A) ->
A;
rank4(_, [], A) ->
A;
rank4([RH | _], [P4, $0, $0, $0], A) ->
[P4, RH | A];
rank4([RH | _], [P4, $0, $0], A) ->
[P4, RH | A];
rank4([RH | _], [P4, $0], A) ->
[P4, RH | A];
rank4([RH | _], [P4], A) ->
[P4, RH | A];
rank4([RH | _], [P4, P3, $0, $0], A) ->
[P3, P4, RH | A];
rank4([RH | _], [P4, P3, $0], A) ->
[P3, P4, RH | A];
rank4([RH | _], [P4, P3], A) ->
[P3, P4, RH | A];
rank4([RH | _], [P4, P3, P2, $0], A) ->
[P2, P3, P4, RH | A];
rank4([RH | _], [P4, P3, P2], A) ->
[P2, P3, P4, RH | A];
rank4([RH | _], [P4, P3, P2, P1], A) ->
[P1, P2, P3, P4, RH | A].
ranks(h) ->
"万億兆京垓秭穣溝澗正載極";
ranks(l) ->
% "曼戌弒俿咳".
"満億照鏡咳".
gaju_mark() -> $木.
puck_mark() -> $本.
one_gaju() -> 1_000_000_000_000_000_000.
-spec read(Format) -> Result
when Format :: string(),
Result :: {ok, Pucks} | {error, Reason},
Pucks :: integer(),
Reason :: {badarg, Partial :: string(), Rest :: term()}
| {incomplete, Partial :: string(), Rest :: binary()}
| format.
%% @doc
%% Convery any valid string formatted representation and output a value in pucks.
%% This routine can fail in the special case of `ch' style formatting with a single
%% comma and/or a single period in it, as this can trigger misinterpretation as `us'
%% style. When in doubt, always call `read/2' with a style specified.
read(Format) ->
case assess_style(string:trim(Format)) of
us -> read(us, Format);
ch -> read(ch, Format);
jp -> read(jp, Format);
Error -> Error
end.
assess_style([H1, H2 | Numbers])
when H1 =:= $木 orelse H1 =:= $本 orelse H2 =:= $木 orelse H2 =:= $本 ->
case count($., Numbers) > 1 of
false -> us;
true -> ch
end;
assess_style(Format) ->
case lists:member($木, Format) orelse lists:member($本, Format) of
true -> jp;
false -> {error, format}
end.
count(Char, String) -> count(Char, String, 0).
count(C, [C | T], A) -> count(C, T, A + 1);
count(C, [_ | T], A) -> count(C, T, A);
count(_, [], A) -> A.
-spec read(Style, Format) -> Result
when Style :: us | ch | jp | undefined,
Format :: string(),
Result :: {ok, Pucks} | {error, Reason},
Pucks :: integer(),
Reason :: {badarg, Partial :: string(), Rest :: term()}
| {incomplete, Partial :: string(), Rest :: binary()}
| format.
%% @doc
%% Convert any valid string formatted representation and output a value in pucks.
%% Note that this function is deliberately a bit too permissive in the case of
%% western formatting, stripping all non-halfwidth digit characters on the high
%% and low sides of the format once the break (decimal) character is identified
%% and sign is determined. That is to say, there are many ways to feed this wacky
%% strings and get a number out of it, so be aware.
read(Style, Format) ->
case unicode:characters_to_list(Format) of
String when is_list(String) ->
Trimmed = string:trim(String),
read2(Style, Trimmed);
{error, Partial, Rest} ->
{error, {badarg, Partial, Rest}};
Incomplete ->
{error, Incomplete}
end.
read2(us, Format) ->
read_western($., Format);
read2(ch, Format) ->
read_western($,, Format);
read2(jp, Format) ->
read_jp(Format);
read2(undefined, Format) ->
read(Format).
read_western(Break, [$-, Format]) ->
case read_western2(Break, Format) of
{ok, Pucks} -> {ok, Pucks * -1};
Error -> Error
end;
read_western(Break, Format) ->
read_western2(Break, Format).
read_western2(Break, Format) ->
case string:split(Format, [Break], all) of
[[], L] -> read_western3(0, L);
[H, []] -> read_western3(H, 0);
[H, L] -> read_western3(H, L);
[H] -> read_western3(H, 0);
_ -> {error, format}
end.
read_western3(0, L) ->
read_l(L);
read_western3(H, 0) ->
case read_h(H) of
{ok, Gajus} -> {ok, Gajus * one_gaju()};
Error -> Error
end;
read_western3(H, L) ->
Gajus = read_h(H),
Pucks = read_l(L),
{ok, (Gajus * one_gaju()) + Pucks}.
read_h(S) ->
case lists:filter(fun is_numchar/1, S) of
[] -> 0;
F -> list_to_integer(Filtered)
end.
read_l(L) ->
case lists:filter(fun is_numchar/1, S) of
[] -> 0;
F -> list_to_integer(lists:flatten(string:pad(F, 18, trailing, $0)))
end.
is_numchar(C) -> $0 =< C andalso C =< $9.
read_jp([$-, Format]) ->
read_jp_neg(Format);
read_jp([$, Format]) ->
read_jp_neg(Format);
read_jp(Format) ->
read_jp2(Format).
read_jp_neg(Format) ->
case read_jp2(Format) of
{ok, Pucks} -> {ok, Pucks * -1};
Error -> Error
end.
read_jp2(Format) ->
case segment_jp(Format) of
{ok, Segments} -> assemble_jp(Segments);
Error -> Error
end.
segment_jp(Format) ->
case string:split(Format, [gaju_mark()], all) of
[Gajus, Pucks] ->
case read_segment(Gajus) of
{ok, GajuSegments} ->
case read_segment(Pucks) of
{ok, PuckSegments} -> {ok, GajuSegments, PuckSegments};
Error -> Error
end;
Error ->
Error
end;
[Gajus] ->
case read_segment(Gajus) of
{ok, GajuSegments} -> {ok, GajuSegments, ["0"]};
Error -> Error
end;
[] ->
{ok, ["0"], ["0"]};
_ ->
{error, format}
end.
read_segment(
assemble_jp({GajuSegments, PuckSegments}) ->
GajuString = lists:flatten(lists:map(fun expand_jp_myriad/1, GajuSegments)),
PuckString = lists:flatten(lists:map(fun expand_jp_myriad/1, PuckDegments)),
{ok, integer_to_list(lists:append(GajuString, PuckString))}.
assemble_jp(PuckSegments) ->
PuckString = lists:flatten(lists:map(fun expand_jp_myriad/1, PuckDegments)),
{ok, integer_to_list(PuckString)}.
expand_jp_myriad(String) ->
string:pad(String, 4, leading, $0).
hw_jp_numchar(C) when $ =< C andalso C =< $ ->
C - $;
hw_jp_numchar(C) ->
C.
-spec price_to_string(Pucks) -> Gajus
when Pucks :: integer(),
Gajus :: string().
%% @doc
%% A simplified formatting function that converts an integer value in Pucks to a string representation
%% in Gajus. Useful for formatting generic output for UI elements
price_to_string(Pucks) ->
Gaju = 1_000_000_000_000_000_000,
H = integer_to_list(Pucks div Gaju),
R = Pucks rem Gaju,
case string:strip(lists:flatten(io_lib:format("~18..0w", [R])), right, $0) of
[] -> H;
T -> string:join([H, T], ".")
end.
-spec string_to_price(Gajus) -> Pucks
when Gajus :: string(),
Pucks :: integer().
%% @doc
%% A simplified formatting function that converts a Gaju value represented as a string to an
%% integer value in Pucks.
string_to_price(String) ->
case string:split(String, ".") of
[H] -> join_price(H, "0");
[H, T] -> join_price(H, T);
_ -> {error, bad_price}
end.
join_price(H, T) ->
try
Parts = [H, string:pad(T, 18, trailing, $0)],
Price = list_to_integer(unicode:characters_to_list(Parts)),
case Price < 0 of
false -> {ok, Price};
true -> {error, negative_price}
end
catch
error:R -> {error, R}
end.