defmodule Localize.Collation.SortKey do @moduledoc """ Constructs binary sort keys from processed collation elements. Sort keys are multi-level byte sequences that can be compared with binary comparison (`<`, `>`, `==`) to determine string ordering. Structure: [L1 weights] 0000 [L2 weights] 0000 [L3 weights] [0000 L4 weights] """ import Bitwise alias Localize.Collation.{Element, Options} @level_separator <<0x00, 0x00>> @doc """ Build a binary sort key from processed collation elements. ### Arguments * `processed_elements` - a list of `{element, quaternary}` tuples as returned by `Localize.Collation.Variable.process/3`. * `options` - a `%Localize.Collation.Options{}` struct controlling which levels to include. * `original_string` - the original input string, used for the identical level (default: `nil`). ### Returns A binary sort key where levels are separated by `<<0x00, 0x00>>`. ### Examples iex> elements = [{{0x23EC, 0x0020, 0x0008, false}, 0}] iex> options = Localize.Collation.Options.new(strength: :primary) iex> Localize.Collation.SortKey.build(elements, options) <<0x23, 0xEC>> """ @spec build([{Element.t(), non_neg_integer()}], Options.t(), String.t() | nil) :: binary() def build(processed_elements, %Options{} = options, original_string \\ nil) do key = build_primary(processed_elements) key = if options.strength in [:secondary, :tertiary, :quaternary, :identical] do key <> @level_separator <> build_secondary(processed_elements, options) else key end key = if options.strength in [:tertiary, :quaternary, :identical] do key = if options.case_level do key <> @level_separator <> build_case_level(processed_elements) else key end key <> @level_separator <> build_tertiary(processed_elements, options) else if options.case_level do key <> @level_separator <> build_case_level(processed_elements) else key end end key = if options.strength in [:quaternary, :identical] and options.alternate == :shifted do key <> @level_separator <> build_quaternary(processed_elements) else key end key = if options.strength == :identical do nfd = if original_string, do: :unicode.characters_to_nfd_binary(original_string), else: <<>> key <> @level_separator <> nfd else key end key end defp build_primary(elements) do elements |> Enum.reduce(<<>>, fn {elem, _q}, acc -> p = Element.primary(elem) if p > 0 do acc <> <> else acc end end) end defp build_secondary(elements, options) do weights = elements |> Enum.reduce([], fn {elem, _q}, acc -> s = Element.secondary(elem) if s > 0 do [s | acc] else acc end end) |> Enum.reverse() weights = if options.backwards do Enum.reverse(weights) else weights end weights |> Enum.reduce(<<>>, fn w, acc -> acc <> <> end) end defp build_tertiary(elements, options) do elements |> Enum.reduce(<<>>, fn {elem, _q}, acc -> t = apply_case_first(Element.tertiary(elem), options.case_first) if t > 0 do acc <> <> else acc end end) end defp build_case_level(elements) do elements |> Enum.reduce(<<>>, fn {elem, _q}, acc -> if Element.primary(elem) > 0 do case_bit = if (Element.tertiary(elem) &&& 0x08) != 0, do: 1, else: 0 acc <> <> else acc end end) end defp build_quaternary(elements) do elements |> Enum.reduce(<<>>, fn {_elem, q}, acc -> if q > 0 do acc <> <> else acc end end) end defp apply_case_first(tertiary, false), do: tertiary defp apply_case_first(tertiary, :upper) do case_bit = tertiary &&& 0x08 if case_bit != 0 do tertiary &&& Bitwise.bnot(0x08) else tertiary ||| 0x08 end end defp apply_case_first(tertiary, :lower) do tertiary end end