Packages
moar
1.0.0
5.0.0
4.3.0
4.2.0
4.1.0
4.0.0
3.2.0
3.1.0
3.0.0
2.7.0
2.6.0
2.5.0
2.4.2
2.4.1
2.4.0
2.3.0
2.2.0
2.1.0
2.0.1
2.0.0
1.64.0
1.63.1
1.63.0
1.62.0
1.61.2
1.61.1
1.61.0
1.60.0
1.59.2
1.59.1
1.59.0
1.58.0
1.57.0
1.56.2
1.56.1
1.56.0
1.55.0
1.54.0
1.53.0
1.52.1
1.52.0
1.51.0
1.50.0
1.49.0
1.48.0
1.47.0
1.46.0
1.45.1
1.45.0
1.44.0
1.43.0
1.42.0
1.41.0
1.40.0
1.39.0
1.38.0
1.37.0
1.36.0
1.35.0
1.34.0
1.33.0
1.32.0
1.31.0
1.30.0
1.29.0
1.28.0
1.27.0
1.26.0
1.25.0
1.24.1
1.24.0
1.23.0
1.22.0
1.21.0
1.20.0
1.19.3
1.19.2
1.19.1
1.19.0
1.18.1
1.18.0
1.17.0
1.16.0
1.15.0
1.14.0
1.13.1
1.13.0
1.12.0
1.11.0
1.10.0
1.9.0
1.8.0
1.7.0
1.6.0
1.5.0
1.4.0
1.3.0
1.2.0
1.1.0
1.0.0
0.1.0
A dependency-free utility library containing 100+ useful functions.
Current section
Files
Jump to
Current section
Files
lib/string.ex
defmodule Moar.String do
# @related [test](/test/string_test.exs)
@moduledoc "String-related functions."
use Bitwise
@doc """
Convert strings and atoms to dash-case (kebab-case) and trims leading and trailing non-alphanumeric characters.
```elixir
iex> ["foo", "FOO", :foo] |> Enum.map(&Moar.String.dasherize/1)
["foo", "foo", "foo"]
iex> ["foo-bar", "foo_bar", :foo_bar, " fooBar ", " ?foo ! bar "] |> Enum.map(&Moar.String.dasherize/1)
["foo-bar", "foo-bar", "foo-bar", "foo-bar", "foo-bar"]
```
"""
@spec dasherize(atom() | binary()) :: binary()
def dasherize(term) do
term
|> Moar.Atom.to_string()
|> String.replace(~r/([A-Z]+)([A-Z][a-z])/, "\\1_\\2")
|> String.replace(~r/([a-z\d])([A-Z])/, "\\1_\\2")
|> String.replace(~r{[^a-z0-9]+}i, "-")
|> String.trim_leading("-")
|> String.trim_trailing("-")
|> String.downcase()
end
@doc """
Truncate `s` to `max_length` by replacing the middle of the string with `replacement`, which defaults to
the single unicode character `…`.
Note that the final length of the string will be `max_length` plus the length of `replacement`.
```elixir
iex> Moar.String.inner_truncate("abcdefghijklmnopqrstuvwxyz", 10)
"abcde…vwxyz"
iex> Moar.String.inner_truncate("abcdefghijklmnopqrstuvwxyz", 10, "<==>")
"abcde<==>vwxyz"
```
"""
@spec inner_truncate(binary(), integer(), binary()) :: binary()
def inner_truncate(s, max_length, replacement \\ "…")
def inner_truncate(nil, _, _),
do: nil
def inner_truncate(s, max_length, replacement) do
case String.length(s) <= max_length do
true ->
s
false ->
left_length = (max_length / 2) |> Float.ceil() |> round()
right_length = (max_length / 2) |> Float.floor() |> round()
[String.slice(s, 0, left_length), replacement, String.slice(s, -right_length, right_length)] |> to_string()
end
end
@doc """
Compares the two binaries in constant-time to avoid timing attacks.
See: <http://codahale.com/a-lesson-in-timing-attacks/>.
```elixir
iex> Moar.String.secure_compare("foo", "bar")
false
```
"""
@spec secure_compare(binary(), binary()) :: boolean()
def secure_compare(left, right) when is_nil(left) or is_nil(right),
do: false
def secure_compare(left, right) when is_binary(left) and is_binary(right),
do: byte_size(left) == byte_size(right) and secure_compare(left, right, 0)
defp secure_compare(<<x, left::binary>>, <<y, right::binary>>, acc) do
xorred = Bitwise.bxor(x, y)
secure_compare(left, right, acc ||| xorred)
end
defp secure_compare(<<>>, <<>>, acc),
do: acc === 0
@doc """
Trims a string and replaces consecutive whitespace characters with a single space.
```elixir
iex> Moar.String.squish(" foo bar \tbaz ")
"foo bar baz"
```
"""
@spec squish(binary()) :: binary()
def squish(nil),
do: nil
def squish(s),
do: s |> trim() |> Elixir.String.replace(~r/\s+/, " ")
@doc """
Adds `surrounder` to the beginning and end of `s`.
```elixir
iex> Moar.String.surround("Hello", "**")
"**Hello**"
```
"""
@spec surround(binary(), binary()) :: binary()
def surround(s, surrounder),
do: surrounder <> s <> surrounder
@doc """
Adds `prefix` to the beginning of `s` and `suffix` to the end.
```elixir
iex> Moar.String.surround("Hello", "“", "”")
"“Hello”"
```
"""
@spec surround(binary(), binary(), binary()) :: binary()
def surround(s, prefix, suffix),
do: prefix <> s <> suffix
@doc """
Converts a string to an integer. Returns `nil` if the argument is `nil` or empty string.
```elixir
iex> Moar.String.to_integer("12,345")
12_345
```
"""
@spec to_integer(nil | binary()) :: integer()
def to_integer(nil),
do: nil
def to_integer(""),
do: nil
def to_integer(s) when is_binary(s),
do: s |> trim() |> Elixir.String.replace(",", "") |> Elixir.String.to_integer()
@doc """
Like `to_integer/1` but with options:
* `:lenient` option removes non-digit characters first
* `default:` option specifies a default in case `s` is nil
```elixir
iex> Moar.String.to_integer("USD$25", :lenient)
25
iex> Moar.String.to_integer(nil, default: 0)
0
```
"""
@spec to_integer(binary(), :lenient | [default: binary()]) :: integer()
def to_integer(s, :lenient) when is_binary(s),
do: s |> String.replace(~r|\D|, "") |> Elixir.String.to_integer()
def to_integer(s, default: default),
do: s |> to_integer() |> Moar.Term.presence(default)
@doc "Like `String.trim/1` but returns `nil` if the argument is nil."
@spec trim(nil | binary()) :: binary()
def trim(nil),
do: nil
def trim(s) when is_binary(s),
do: Elixir.String.trim(s)
@doc """
Truncates `s` at the last instance of `at`, causing the string to be at most `limit` characters.
```elixir
iex> Moar.String.truncate_at("I like apples. I like bananas. I like cherries.", ".", 35)
"I like apples. I like bananas."
```
"""
def truncate_at(s, at, limit) do
s
|> String.graphemes()
|> Enum.take(limit)
|> Enum.reverse()
|> Enum.split_while(fn c -> c != at end)
|> case do
{a, []} -> a
{[], b} -> b
{_a, b} -> b
end
|> Enum.reverse()
|> Enum.join("")
end
end