One of the joys of KAZOO is getting to learn about how telecom works in the wider world. This forces Unicode front and center when dealing with user input (on the plus side, we can use emojis for phone extensions!).
Looking at our code, we see we've naively handled just the Latin-based alphabet. For inspiration, we open up string.erl to see how it handles upper/lowercase conversions:
%% ISO/IEC 8859-1 (latin1) letters are converted, others are ignored
%%
to_lower_char(C) when is_integer(C), $A =< C, C =< $Z ->
C + 32;
to_lower_char(C) when is_integer(C), 16#C0 =< C, C =< 16#D6 ->
C + 32;
to_lower_char(C) when is_integer(C), 16#D8 =< C, C =< 16#DE ->
C + 32;
to_lower_char(C) ->
C.
to_upper_char(C) when is_integer(C), $a =< C, C =< $z ->
C - 32;
to_upper_char(C) when is_integer(C), 16#E0 =< C, C =< 16#F6 ->
C - 32;
to_upper_char(C) when is_integer(C), 16#F8 =< C, C =< 16#FE ->
C - 32;
to_upper_char(C) ->
C.
Let's adjust our generators first to see failing cases:
lower_char() ->
?LET(Lower
,union([choose($a,$z)
,choose(16#E0,16#F6)
,choose(16#F8,16#FE)
])
,{Lower, [Lower]}
).
first_upper_char() ->
?LET(Upper
,union([choose($A,$Z)
,choose(16#C0,16#D6)
,choose(16#D8,16#DE)
])
,{Upper, [Upper+32]}
).
upper_char() ->
?LET(Upper
,union([choose($A,$Z)
,choose(16#C0,16#D6)
,choose(16#D8,16#DE)
])
,{Upper, [$_, Upper+32]}
).
Running this, we get some nice failures:
proper_test_ (prop_morph)...!
Failed: After 1 test(s).
{<<222,240,240,198,253,220,75,212,233,76,248,110,77,83,229,99,195,88,216,250,246,67,227,237,103,240,217,253,220,221>>,<<254,240,240,95,230,253,95,252,95,107,95,244,233,95,108,248,110,95,109,95,115,229,99,95,227,95,120,95,248,250,246,95,99,227,237,103,240,95,249,253,95,252,95,253>>}
failed to morph 'ÞððÆýÜKÔéLønMSåcÃXØúöCãígðÙýÜÝ' to 'þðð_æý_ü_k_ôé_løn_m_såc_ã_x_øúö_cãígð_ùý_ü_ý'
Shrinking ..............................................................(62 time(s))
{<<192,65,65,65,65,65,65,65,65,65,65,65,65,65,65,65,65,65,65,65,65,65,65,65,65,65,65,65,65,65>>,<<224,95,97,95,97,95,97,95,97,95,97,95,97,95,97,95,97,95,97,95,97,95,97,95,97,95,97,95,97,95,97,95,97,95,97,95,97,95,97,95,97,95,97,95,97,95,97,95,97,95,97,95,97,95,97,95,97,95,97>>}
failed to morph 'ÀAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' to 'à_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a'
You can also see that, because we wanted 30-character strings, PropEr generates a shrunk version that is still 30 characters long. Let's inform PropEr that we want to constrain the length of the strings but be a little more flexible so PropEr can generate better failing test cases:
prop_morph() ->
?FORALL({Camel, Snake}
,resize(20, camel_and_snake())
,camel_and_snake()
,?WHENFAIL(io:format('user', "~nfailed to morph '~s' to '~s'~n", [Camel, Snake])
,Snake =:= camel_to_snake(Camel)
)
).
camel_and_snake() ->
?SIZED(Length, camel_and_snake(Length)).
You can read more about resize/2
and the ?SIZED
macro but basically they let PropEr know to constrain the size a bit but with more flexibility than a static length.
Running the tests now:
proper_test_ (prop_morph)...!
Failed: After 1 test(s).
{<<220>>,<<252>>}
failed to morph 'Ü' to 'ü'
Shrinking ..(2 time(s))
{<<192>>,<<224>>}
failed to morph 'À' to 'à'
Much easier!
Let's adjust our implementation to account for these upper/lower bounds:
is_upper_char(Char) ->
(Char >= $A andalso Char =< $Z)
orelse (16#C0 =< Char andalso Char =< 16#D6)
orelse (16#D8 =< Char andalso Char =< 16#DE).
to_lower_char(Char) when is_integer(Char), $A =< Char, Char =< $Z -> Char + 32;
to_lower_char(Char) when is_integer(Char), 16#C0 =< Char, Char =< 16#D6 ->
Char + 32;
to_lower_char(Char) when is_integer(Char), 16#D8 =< Char, Char =< 16#DE ->
Char + 32;
to_lower_char(Char) -> Char.
And the tests now pass nicely:
proper_test_ (prop_morph) .......................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................
OK: Passed 500 test(s).