defmodule LZString do use LZString.Base64 @compress_dict %{ size_8: 0, size_16: 1, eof: 2 } @decompress_dict Enum.into(@compress_dict, %{}, fn {k, v} -> {v, k} end) @size_8 @compress_dict[:size_8] @size_16 @compress_dict[:size_16] @eof @compress_dict[:eof] @doc ~S""" Compresses the given String with the lz-string algorithm. iex> LZString.compress("hello, i am a 猫") <<5, 133, 48, 54, 96, 246, 3, 64, 4, 9, 107, 2, 24, 22, 217, 180, 53, 51, 144, 0>> """ @spec compress(String.t()) :: binary def compress(""), do: "" def compress(str) do output = compress("", str, @compress_dict) |> :erlang.list_to_bitstring() # the js implementation incorrectly adds padding when none is needed, so we do too. padding_bits = 16 - (output |> bit_size |> rem(16)) # padding_bits = # case 16 - (output |> bit_size |> rem(16)) do # 16 -> 0 # n -> n # end <> end def compress(w, <> <> rest, dict) do c = <> char_just_added = !Map.has_key?(dict, c) dict = if char_just_added do Map.put(dict, c, {:first_time, map_size(dict)}) else dict end wc = w <> c if Map.has_key?(dict, wc) do compress(wc, rest, dict) else {dict, output} = w_output(w, dict, char_just_added) dict = Map.put(dict, wc, map_size(dict)) [output | compress(c, rest, dict)] end end def compress(w, "", dict) do size = num_bits(map_size(dict) - 1) {_dict, output} = w_output(w, dict, false) [output, reverse(<>)] end defp w_output([], _dict, _char_just_added), do: <<>> defp w_output(w, dict, char_just_added) do case Map.fetch(dict, w) do {:ok, {:first_time, dict_index}} -> dict = Map.put(dict, w, dict_index) marker_size = num_bits(dict_index) <> = w {size_marker, char_size} = if num_bits(char_val) <= 8 do {dict[:size_8], 8} else {dict[:size_16], 16} end size_marker_bits = reverse(<>) char_bits = reverse(<>) {dict, <>} {:ok, dict_index} -> map_size = map_size(dict) - 1 # a char just being added to the dict may cause us to add an extra bit # to the dict_index output where one isn't strictly needed yet map_size = if char_just_added do map_size - 1 else map_size end size = num_bits(map_size) {dict, reverse(<>)} end end @doc ~S""" Decompresses the given binary with the lz-string algorithm. iex> LZString.decompress(<<5, 133, 48, 54, 96, 246, 3, 64, 4, 9, 107, 2, 24, 22, 217, 180, 53, 51, 144, 0>>) "hello, i am a 猫" """ @spec decompress(binary) :: String.t() def decompress(""), do: "" def decompress(str) do {:char, c, rest, dict} = decode_next_segment(str, @decompress_dict) decompress(c, rest, dict) |> :erlang.list_to_binary() |> :unicode.characters_to_binary(:utf16) end def decompress(w, str, dict) do case decode_next_segment(str, dict) do {:char, c, rest, dict} -> dict = Map.put(dict, map_size(dict), w <> c) [w | decompress(c, rest, dict)] {:seq, seq, rest} -> c = case Map.fetch(dict, seq) do {:ok, decompressed} -> decompressed :error -> unless map_size(dict) == seq, do: raise("unknown sequence index #{seq}") w <> first_utf16(w) end dict = Map.put(dict, map_size(dict), w <> first_utf16(c)) [w | decompress(c, rest, dict)] :eof -> [w] end end defp decode_next_segment(str, dict) do size = dict |> map_size |> num_bits <> = str # dict_entry is in LSB format, bring it back to MSB <> = reverse(<>) case dict_entry do @size_8 -> <> = rest <> = reverse(<>) char = <> dict = Map.put(dict, map_size(dict), char) {:char, char, rest, dict} @size_16 -> <> = rest <> = reverse(<>) char = <> dict = Map.put(dict, map_size(dict), char) {:char, char, rest, dict} @eof -> :eof index -> {:seq, index, rest} end end defp first_utf16(<> <> _) do <> end defp num_bits(0), do: 1 defp num_bits(int) do int |> :math.log2() |> trunc |> Kernel.+(1) end # http://erlang.org/euc/07/papers/1700Gustafsson.pdf defp reverse(<<>>), do: <<>> defp reverse(<>) do <> end def debug(bitstring) do for <>, do: bit end end