defmodule Dmp.StringUtils do @moduledoc """ Java.String- and Javascript-compatible functions missing in Elixir's `String` module. """ @doc """ Returns the index within this string of the first occurrence of the specified substring, or -1 if there is no such occurence. ## Examples ies> StringUtils.index_of("", "") 0 iex> StringUtils.index_of("abracadabra", "b") 1 iex> StringUtils.index_of("abracadabra", "f") -1 """ @spec index_of(String.t(), String.t()) :: integer() def index_of(s, str), do: index_of(s, str, 0) @doc """ Returns the index within this string of the first occurrence of the specified substring, starting the search at the specified index, or -1 if there is no such occurence. ## Examples iex> StringUtils.index_of("abracadabra", "", 2) 2 iex> StringUtils.index_of("abracadabra", "", 100) 11 iex> StringUtils.index_of("abracadabra", "b", 2) 8 iex> StringUtils.index_of("abracadabra", "f", 2) -1 """ @spec index_of(String.t(), String.t(), non_neg_integer()) :: integer() def index_of(s, "", from_index), do: min(from_index, String.length(s)) def index_of("", _, _), do: -1 def index_of(s, str, from_index) when is_integer(from_index) and from_index >= 0 do case String.split(substring(s, from_index), str, parts: 2) do [before, _] -> String.length(before) + from_index _ -> -1 end end @doc """ Returns the index within this string of the last occurrence of the specified substring, or -1 if there is no such occurence. ## Examples ies> StringUtils.last_index_of("", "") 0 ies> StringUtils.last_index_of("abracadabra", "") 11 iex> StringUtils.last_index_of("abracadabra", "b") 8 iex> StringUtils.last_index_of("abracadabra", "f") -1 """ @spec last_index_of(String.t(), String.t()) :: integer() def last_index_of(s, str), do: last_index_of(s, str, 0) @doc """ Returns the index within this string of the last occurrence of the specified substring, starting the search at the specified index, or -1 if there is no such occurence. ## Examples iex> StringUtils.index_of("abracadabra", "b", 5) 8 iex> StringUtils.index_of("abracadabra", "d", 9) -1 iex> StringUtils.last_index_of("abcdefghijk", "fgh", 5) 5 ies> StringUtils.last_index_of("abcdefghijk", "", 5) 11 """ @spec last_index_of(String.t(), String.t(), non_neg_integer()) :: integer() def last_index_of(s, "", _), do: String.length(s) def last_index_of("", _, _), do: -1 def last_index_of(s, str, begin_index) when begin_index < 0, do: last_index_of(s, str, 0) def last_index_of(s, str, begin_index) do s = if begin_index > 0 do String.slice(s, begin_index..-1//1) else s end s_rev = String.reverse(s) str_rev = String.reverse(str) case index_of(s_rev, str_rev) do -1 -> -1 i -> begin_index + (String.length(s_rev) - String.length(str_rev) - i) end end @doc """ Returns a new string that is a substring of this string. The substring begins with the character at the specified index and extends to the end of this string. ## Examples iex> StringUtils.substring("abracadabra", 6) "dabra" iex> StringUtils.substring("abracadabra", 12) "" """ @spec substring(String.t(), non_neg_integer()) :: String.t() def substring(s, begin_index), do: String.slice(s, begin_index..-1//1) @doc """ Returns a new string that is a substring of this string. The substring begins at the specified `begin_index` and extends to the character at index `end_index - 1`. Thus the length of the substring is `end_index - begin_index`. ## Examples iex> StringUtils.substring("abracadabra", 2, 6) "raca" iex> StringUtils.substring("abracadabra", 2, 12) "racadabra" """ @spec substring(String.t(), non_neg_integer(), non_neg_integer()) :: String.t() def substring(s, begin_index, end_index) do if end_index > begin_index do String.slice(s, begin_index..(end_index - 1)) else "" end end @not_escaped [ {"+", " "}, {"%2A", "*"} ] @doc """ A URI encoding, but with spaces and asterisks left as is, for use with diffs. ## Examples iex> StringUtils.uri_encode("(") "%28" iex> StringUtils.uri_encode(" ") " " iex> StringUtils.uri_encode("*") "*" iex> StringUtils.uri_encode("[]") "%5B%5D" """ @spec uri_encode(String.t()) :: String.t() def uri_encode(str) do str = URI.encode_www_form(str) Enum.reduce(@not_escaped, str, fn {from, to}, acc -> String.replace(acc, from, to) end) end @unescaped [ {"%21", "!"}, {"%7E", "~"}, {"%27", "'"}, {"%28", "("}, {"%29", ")"}, {"%3B", ";"}, {"%2F", "/"}, {"%3F", "?"}, {"%3A", ":"}, {"%40", "@"}, {"%26", "&"}, {"%3D", "="}, {"%2B", "+"}, {"%24", "$"}, {"%2C", ","}, {"%23", "#"} ] @doc """ Unescape selected chars for compatability with JavaScript's `encodeURI`. In speed critical applications this could be dropped since the receiving application will certainly decode these fine. Note that this function is case-sensitive. Thus "%3f" would not be unescaped. But this is ok because it is only called with the output of `StringUtils.uri_encode` which returns uppercase hex. ## Examples iex> StringUtils.unescape_for_encode_uri_compatability("%3F") "?" iex> StringUtils.unescape_for_encode_uri_compatability("%3f") "%3f" iex> StringUtils.unescape_for_encode_uri_compatability("%24") "$" """ @spec unescape_for_encode_uri_compatability(String.t()) :: String.t() def unescape_for_encode_uri_compatability(str) do Enum.reduce(@unescaped, str, fn {from, to}, acc -> String.replace(acc, from, to) end) end end