diff options
author | inoas <mail@inoas.com> | 2022-05-17 18:48:03 +0000 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-05-17 19:48:03 +0100 |
commit | 5c26af70c60c51531972a760482a3580402b7d68 (patch) | |
tree | 50f1df6f5f1c3f09e550a977b45d510eb0a3a6c1 | |
parent | f9d57db847ff2a8471c9136bd309c870b610d04d (diff) | |
download | gleam_stdlib-5c26af70c60c51531972a760482a3580402b7d68.tar.gz gleam_stdlib-5c26af70c60c51531972a760482a3580402b7d68.zip |
add string.inspect() (#296)
-rw-r--r-- | CHANGELOG.md | 1 | ||||
-rw-r--r-- | src/gleam/string.gleam | 15 | ||||
-rw-r--r-- | src/gleam_stdlib.erl | 68 | ||||
-rw-r--r-- | test/gleam/string_test.gleam | 281 |
4 files changed, 359 insertions, 6 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 2457140..4f58fbd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - The `float` module gains the `divide` function. - The `int` module gains the `divide`, `power`, and `square_root` functions. +- The `string` module gains the `first`, `last`, `capitalise` and `inspect` functions. - The `string` module gains the `first`, `last`, and `capitalise` functions. - Fixed a bug where `string.reverse` would break utf8 strings on target JavaScript. - Fixed a bug where `string.slice` would break utf8 strings on target JavaScript. diff --git a/src/gleam/string.gleam b/src/gleam/string.gleam index 45d4d31..d06e2dd 100644 --- a/src/gleam/string.gleam +++ b/src/gleam/string.gleam @@ -823,3 +823,18 @@ pub fn capitalise(s: String) -> String { _ -> "" } } + +pub fn inspect(value: a) -> String { + do_inspect(value) + |> string_builder.to_string +} + +if javascript { + external fn do_inspect(value: a) -> string_builder.StringBuilder = + "../gleam.mjs" "inspect" +} + +if erlang { + external fn do_inspect(value: a) -> string_builder.StringBuilder = + "gleam_stdlib" "inspect" +} diff --git a/src/gleam_stdlib.erl b/src/gleam_stdlib.erl index cc1475f..9dc769b 100644 --- a/src/gleam_stdlib.erl +++ b/src/gleam_stdlib.erl @@ -9,7 +9,8 @@ bit_string_slice/3, decode_bit_string/1, compile_regex/2, regex_scan/2, percent_encode/1, percent_decode/1, regex_check/2, regex_split/2, base_decode64/1, parse_query/1, bit_string_concat/1, size_of_tuple/1, - decode_tuple/1, tuple_get/2, classify_dynamic/1, print/1, println/1]). + decode_tuple/1, tuple_get/2, classify_dynamic/1, print/1, println/1, + inspect/1]). %% Taken from OTP's uri_string module -define(DEC2HEX(X), @@ -47,9 +48,9 @@ classify_dynamic(X) when is_float(X) -> <<"Float">>; classify_dynamic(X) when is_list(X) -> <<"List">>; classify_dynamic(X) when is_boolean(X) -> <<"Bool">>; classify_dynamic(X) when is_map(X) -> <<"Map">>; -classify_dynamic(X) when is_tuple(X) -> +classify_dynamic(X) when is_tuple(X) -> iolist_to_binary(["Tuple of ", integer_to_list(tuple_size(X)), " elements"]); -classify_dynamic(X) when +classify_dynamic(X) when is_function(X, 0) orelse is_function(X, 1) orelse is_function(X, 2) orelse is_function(X, 3) orelse is_function(X, 4) orelse is_function(X, 5) orelse is_function(X, 6) orelse is_function(X, 7) orelse is_function(X, 8) orelse @@ -81,7 +82,7 @@ decode_list(Data) -> decode_error_msg(<<"List">>, Data). decode_field(Data, Key) -> case Data of #{Key := Value} -> {ok, Value}; - _ -> + _ -> decode_error(<<"field"/utf8>>, <<"nothing"/utf8>>) end. @@ -227,7 +228,7 @@ wrap_list(X) -> [X]. parse_query(Query) -> case uri_string:dissect_query(Query) of {error, _, _} -> {error, nil}; - Pairs -> + Pairs -> Pairs1 = lists:map(fun ({K, true}) -> {K, <<"">>}; (Pair) -> Pair @@ -296,7 +297,7 @@ uri_parse(String) -> maps_get_optional(Uri, userinfo), maps_get_optional(Uri, host), maps_get_optional(Uri, port), - maps_get_or(Uri, path, <<>>), + maps_get_or(Uri, path, <<>>), maps_get_optional(Uri, query), maps_get_optional(Uri, fragment) }} @@ -319,3 +320,58 @@ print(String) -> println(String) -> io:put_chars([String, $\n]), nil. + +inspect(true) -> + "True"; +inspect(false) -> + "False"; +inspect(Any) when is_atom(Any) -> + lists:map( + fun(Part) -> + [Head | Tail] = string:next_grapheme(unicode:characters_to_binary(Part)), + [string:uppercase([Head]), Tail] + end, + re:split(erlang:atom_to_list(Any), "_+", [{return, iodata}]) + ); +inspect(Any) when is_integer(Any) -> + erlang:integer_to_list(Any); +inspect(Any) when is_float(Any) -> + io_lib_format:fwrite_g(Any); +inspect(Any) when is_binary(Any) -> + Pattern = [$"], + Replacement = [$\\, $\\, $"], + Escaped = re:replace(Any, Pattern, Replacement, [{return, binary}, global]), + ["\"", Escaped, "\""]; +inspect(Any) when is_list(Any) -> + ["[", + lists:join(<<", ">>, + lists:map(fun inspect/1, Any) + ), + "]"]; +inspect(Any) when is_tuple(Any) % Record constructors + andalso is_atom(element(1, Any)) + andalso element(1, Any) =/= false + andalso element(1, Any) =/= true + andalso element(1, Any) =/= nil +-> + [Atom | ArgsList] = erlang:tuple_to_list(Any), + Args = + lists:join(<<", ">>, + lists:map(fun inspect/1, ArgsList) + ), + [inspect(Atom), "(", Args, ")"]; +inspect(Any) when is_tuple(Any) -> + ["#(", + lists:join(<<", ">>, + lists:map(fun inspect/1, erlang:tuple_to_list(Any)) + ), + ")"]; +inspect(Any) when is_function(Any) -> + {arity, Arity} = erlang:fun_info(Any, arity), + ArgsAsciiCodes = lists:seq($a, $a + Arity - 1), + Args = lists:join(<<", ">>, + lists:map(fun(Arg) -> <<Arg>> end, ArgsAsciiCodes) + ), + ["//fn(", Args, ") { ... }"]; +inspect(Any) -> + ["//erl(", io_lib:format("~p", [Any]), ")"]. diff --git a/test/gleam/string_test.gleam b/test/gleam/string_test.gleam index 4c73b41..fafea89 100644 --- a/test/gleam/string_test.gleam +++ b/test/gleam/string_test.gleam @@ -486,3 +486,284 @@ pub fn capitalise_test() { |> string.capitalise |> should.equal("る") } + +type InspectType(a, b) { + InspectTypeZero + InspectTypeOne(a) + InspectTypeTwo(a, b) +} + +pub fn inspect_test() { + string.inspect(True) + |> should.equal("True") + + string.inspect(False) + |> should.equal("False") + + string.inspect([True, False]) + |> should.equal("[True, False]") + + string.inspect([False, False]) + |> should.equal("[False, False]") + + string.inspect([True, True]) + |> should.equal("[True, True]") + + string.inspect([Nil, Nil]) + |> should.equal("[Nil, Nil]") + + string.inspect(#(True, False)) + |> should.equal("#(True, False)") + + string.inspect(#(False, False)) + |> should.equal("#(False, False)") + + string.inspect(#(True, True)) + |> should.equal("#(True, True)") + + string.inspect(#(Nil, True)) + |> should.equal("#(Nil, True)") + + string.inspect(#(Nil, False)) + |> should.equal("#(Nil, False)") + + string.inspect(#(True, Nil)) + |> should.equal("#(True, Nil)") + + string.inspect(#(False, Nil)) + |> should.equal("#(False, Nil)") + + string.inspect(-1) + |> should.equal("-1") + + string.inspect(0) + |> should.equal("0") + + string.inspect(1) + |> should.equal("1") + + string.inspect([]) + |> should.equal("[]") + + string.inspect([1]) + |> should.equal("[1]") + + string.inspect([1, 2]) + |> should.equal("[1, 2]") + + string.inspect([[1], [1]]) + |> should.equal("[[1], [1]]") + + string.inspect(-1.5) + |> should.equal("-1.5") + + string.inspect(1.5) + |> should.equal("1.5") + + string.inspect([1.5]) + |> should.equal("[1.5]") + + string.inspect("") + |> should.equal("\"\"") + + string.inspect("1") + |> should.equal("\"1\"") + + string.inspect("Hello Joe!") + |> should.equal("\"Hello Joe!\"") + + string.inspect("Hello \"Manuel\"!") + |> should.equal("\"Hello \\\"Manuel\\\"!\"") + + string.inspect("💜 Gleam") + |> should.equal("\"💜 Gleam\"") + + string.inspect(["1"]) + |> should.equal("[\"1\"]") + + string.inspect(#()) + |> should.equal("#()") + + string.inspect(#(1)) + |> should.equal("#(1)") + + string.inspect(#("1")) + |> should.equal("#(\"1\")") + + string.inspect(#(1.5)) + |> should.equal("#(1.5)") + + string.inspect([#(1, 2, 3), #(1, 2, 3)]) + |> should.equal("[#(1, 2, 3), #(1, 2, 3)]") + + string.inspect(#([1, 2, 3], "🌈", #(1, "1", True))) + |> should.equal("#([1, 2, 3], \"🌈\", #(1, \"1\", True))") + + string.inspect(Nil) + |> should.equal("Nil") + + string.inspect(Ok(1)) + |> should.equal("Ok(1)") + + string.inspect(Ok(True)) + |> should.equal("Ok(True)") + + string.inspect(Ok(False)) + |> should.equal("Ok(False)") + + string.inspect(Ok(Nil)) + |> should.equal("Ok(Nil)") + + string.inspect(Error(2)) + |> should.equal("Error(2)") + + string.inspect(Error(True)) + |> should.equal("Error(True)") + + string.inspect(Error(False)) + |> should.equal("Error(False)") + + string.inspect(Error(Nil)) + |> should.equal("Error(Nil)") + + string.inspect(InspectTypeZero) + |> should.equal("InspectTypeZero") + + string.inspect(InspectTypeOne(1)) + |> should.equal("InspectTypeOne(1)") + + string.inspect(InspectTypeTwo(1, 2)) + |> should.equal("InspectTypeTwo(1, 2)") + + string.inspect(InspectTypeOne([1])) + |> should.equal("InspectTypeOne([1])") + + string.inspect(InspectTypeOne("1")) + |> should.equal("InspectTypeOne(\"1\")") + + string.inspect(InspectTypeOne(["1"])) + |> should.equal("InspectTypeOne([\"1\"])") + + string.inspect(InspectTypeOne(#([1], "a"))) + |> should.equal("InspectTypeOne(#([1], \"a\"))") + + string.inspect(Ok) + |> should.equal("//fn(a) { ... }") + + string.inspect(Error) + |> should.equal("//fn(a) { ... }") + + string.inspect(fn() { Nil }) + |> should.equal("//fn() { ... }") + + string.inspect(fn(a) { + a + Nil + }) + |> should.equal("//fn(a) { ... }") + + string.inspect(fn(a, b) { + a + b + Nil + }) + |> should.equal("//fn(a, b) { ... }") + + string.inspect(fn(x, y) { + x + y + Nil + }) + |> should.equal("//fn(a, b) { ... }") + + string.inspect(fn(foo: Int, bar: String) -> Bool { + foo + bar + False + }) + |> should.equal("//fn(a, b) { ... }") + + string.inspect(#(InspectTypeOne, InspectTypeTwo)) + |> should.equal("#(//fn(a) { ... }, //fn(a, b) { ... })") + + string.inspect(InspectTypeOne(InspectTypeZero)) + |> should.equal("InspectTypeOne(InspectTypeZero)") +} + +if javascript { + pub fn target_inspect_test() { + // Due to Erlang's internal representation, on Erlang this will pass, instead: + // |> should.equal("InspectTypeZero(InspectTypeZero)") + // + string.inspect(#(InspectTypeZero, InspectTypeZero)) + |> should.equal("#(InspectTypeZero, InspectTypeZero)") + + // Due to JavaScript's `Number` type `Float`s without digits return as `Int`s. + // + string.inspect(-1.0) + |> should.equal("-1") + + string.inspect(0.0) + |> should.equal("0") + + string.inspect(1.0) + |> should.equal("1") + + string.inspect([1.0]) + |> should.equal("[1]") + + string.inspect(#(1.0)) + |> should.equal("#(1)") + } +} + +if erlang { + import gleam/regex + + external fn create_erlang_pid() -> String = + "erlang" "self" + + external fn create_erlang_reference() -> String = + "erlang" "make_ref" + + pub fn target_inspect_test() { + // Erlang's internal representation does not allow a correct differentiation + // |> should.equal("#(InspectTypeZero, InspectTypeZero)") + // + string.inspect(#(InspectTypeZero, InspectTypeZero)) + |> should.equal("InspectTypeZero(InspectTypeZero)") + + // Unlike JavaScript, Erlang correctly differentiates between 1 and 1.0 + // + string.inspect(-1.0) + |> should.equal("-1.0") + + string.inspect(0.0) + |> should.equal("0.0") + + string.inspect(1.0) + |> should.equal("1.0") + + string.inspect([1.0]) + |> should.equal("[1.0]") + + string.inspect(#(1.0)) + |> should.equal("#(1.0)") + + // Looks like `//erl(<0.83.0>)` + assert Ok(regular_expression) = + regex.from_string("^\\/\\/erl\\(<[0-9]+\\.[0-9]+\\.[0-9]+>\\)$") + string.inspect(create_erlang_pid()) + |> regex.check(regular_expression, _) + |> should.equal(True) + + // Looks like: `//erl(#Ref<0.1809744150.4035444737.100468>)` + assert Ok(regular_expression) = + regex.from_string( + "^\\/\\/erl\\(#Ref<[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+>\\)$", + ) + string.inspect(create_erlang_reference()) + |> regex.check(regular_expression, _) + |> should.equal(True) + } +} |