lib/rdf/xsd/datatypes/double.ex

defmodule RDF.XSD.Double do
  @moduledoc """
  `RDF.XSD.Datatype` for `xsd:double`.

  See: <https://www.w3.org/TR/xmlschema11-2/#double>
  """

  @type special_values :: :positive_infinity | :negative_infinity | :nan
  @type valid_value :: float | special_values

  @special_values ~W[positive_infinity negative_infinity nan]a

  use RDF.XSD.Datatype.Primitive,
    name: "double",
    id: RDF.Utils.Bootstrapping.xsd_iri("double")

  alias RDF.XSD

  def_applicable_facet XSD.Facets.MinInclusive
  def_applicable_facet XSD.Facets.MaxInclusive
  def_applicable_facet XSD.Facets.MinExclusive
  def_applicable_facet XSD.Facets.MaxExclusive
  def_applicable_facet XSD.Facets.Pattern

  @doc false
  def min_inclusive_conform?(min_inclusive, value, _lexical) do
    value >= min_inclusive
  end

  @doc false
  def max_inclusive_conform?(max_inclusive, value, _lexical) do
    value <= max_inclusive
  end

  @doc false
  def min_exclusive_conform?(min_exclusive, value, _lexical) do
    value > min_exclusive
  end

  @doc false
  def max_exclusive_conform?(max_exclusive, value, _lexical) do
    value < max_exclusive
  end

  @doc false
  def pattern_conform?(pattern, _value, lexical) do
    XSD.Facets.Pattern.conform?(pattern, lexical)
  end

  @impl XSD.Datatype
  def lexical_mapping(lexical, opts)
  def lexical_mapping("." <> lexical, opts), do: lexical_mapping("0." <> lexical, opts)

  def lexical_mapping(lexical, opts) do
    case Float.parse(lexical) do
      {float, ""} ->
        float

      {float, "."} ->
        float

      {float, remainder} ->
        # 1.E-8 is not a valid Elixir float literal and consequently not fully parsed with Float.parse
        if Regex.match?(~r/^\.e?[\+\-]?\d+$/i, remainder) do
          lexical_mapping(to_string(float) <> String.trim_leading(remainder, "."), opts)
        else
          @invalid_value
        end

      :error ->
        case String.upcase(lexical) do
          "INF" -> :positive_infinity
          "-INF" -> :negative_infinity
          "NAN" -> :nan
          _ -> @invalid_value
        end
    end
  end

  @impl XSD.Datatype
  @spec elixir_mapping(valid_value | integer | any, Keyword.t()) :: value
  def elixir_mapping(value, _)
  def elixir_mapping(value, _) when is_float(value), do: value
  def elixir_mapping(value, _) when is_integer(value), do: :erlang.float(value)
  def elixir_mapping(value, _) when value in @special_values, do: value
  def elixir_mapping(_, _), do: @invalid_value

  @impl XSD.Datatype
  @spec init_valid_lexical(valid_value, XSD.Datatype.uncanonical_lexical(), Keyword.t()) ::
          XSD.Datatype.uncanonical_lexical()
  def init_valid_lexical(value, lexical, opts)
  def init_valid_lexical(value, nil, _) when is_atom(value), do: nil
  def init_valid_lexical(_, nil, _), do: nil
  def init_valid_lexical(_, lexical, _), do: lexical

  @impl XSD.Datatype
  @spec canonical_mapping(valid_value) :: String.t()
  def canonical_mapping(value)

  # Produces the exponential form of a float
  def canonical_mapping(float) when is_float(float) do
    # We can't use simple %f transformation due to special requirements from N3 tests in representation
    [i, f, e] =
      float
      |> float_to_string()
      |> String.split(~r/[\.e]/)

    # remove any trailing zeroes
    f =
      case String.replace(f, ~r/0*$/, "", global: false) do
        # ...but there must be a digit to the right of the decimal point
        "" -> "0"
        f -> f
      end

    e = String.trim_leading(e, "+")

    "#{i}.#{f}E#{e}"
  end

  def canonical_mapping(:nan), do: "NaN"
  def canonical_mapping(:positive_infinity), do: "INF"
  def canonical_mapping(:negative_infinity), do: "-INF"

  defp float_to_string(float) do
    :io_lib.format("~.15e", [float]) |> to_string()
  end

  @impl RDF.Literal.Datatype
  def do_cast(value)

  def do_cast(%XSD.String{} = xsd_string) do
    xsd_string.value |> new() |> canonical()
  end

  def do_cast(literal) do
    cond do
      XSD.Boolean.datatype?(literal) ->
        case literal.value do
          false -> new(0.0)
          true -> new(1.0)
        end

      XSD.Integer.datatype?(literal) ->
        new(literal.value)

      XSD.Decimal.datatype?(literal) ->
        literal.value
        |> Decimal.to_float()
        |> new()

      true ->
        super(literal)
    end
  end

  @impl RDF.Literal.Datatype
  def do_equal_value_same_or_derived_datatypes?(left, right),
    do: XSD.Numeric.do_equal_value?(left, right)

  @impl RDF.Literal.Datatype
  def do_equal_value_different_datatypes?(left, right),
    do: XSD.Numeric.do_equal_value?(left, right)

  @impl RDF.Literal.Datatype
  def do_compare(left, right), do: XSD.Numeric.do_compare(left, right)
end