From ed403d917500e44924d6015993baf35ac7ab3aa0 Mon Sep 17 00:00:00 2001 From: Marcel Otto Date: Sun, 20 Dec 2020 02:55:24 +0100 Subject: [PATCH] Allow initialization of dates and times with timezones from tuples --- CHANGELOG.md | 20 ++++++++++ lib/rdf/xsd/datatypes/date.ex | 17 ++++++--- lib/rdf/xsd/datatypes/time.ex | 54 +++++++++++++++------------ test/unit/xsd/datatypes/date_test.exs | 9 ++++- test/unit/xsd/datatypes/time_test.exs | 12 ++++-- 5 files changed, 76 insertions(+), 36 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d0a7c90..94bdfdd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,26 @@ This project adheres to [Semantic Versioning](http://semver.org/) and [Keep a CHANGELOG](http://keepachangelog.com). +## Unreleased + +### Added + +- `RDF.XSD.Base64Binary` datatype ([@pukkamustard](https://github.com/pukkamustard)) + +### Changed + +- a new option `:as_value` to enforce interpretation of an input string as a value + instead of a lexical, which is needed on datatypes where the lexical space and + the value space both consist of strings +- `RDF.XSD.Date` and `RDF.XSD.Time` both can now be initialized with tuples of an + Elixir `Date` resp. `Time` value and a timezone string (previously XSD date and + time values with time zones could only be created from strings) + + +[Compare v0.9.1...HEAD](https://github.com/rdf-elixir/rdf-ex/compare/v0.9.1...HEAD) + + + ## 0.9.1 - 2020-11-16 Elixir versions < 1.9 are no longer supported diff --git a/lib/rdf/xsd/datatypes/date.ex b/lib/rdf/xsd/datatypes/date.ex index dc1a358..0c039f1 100644 --- a/lib/rdf/xsd/datatypes/date.ex +++ b/lib/rdf/xsd/datatypes/date.ex @@ -56,25 +56,30 @@ defmodule RDF.XSD.Date do end @impl XSD.Datatype - @spec elixir_mapping(Date.t() | any, Keyword.t()) :: + @spec elixir_mapping(Date.t() | valid_value | any, Keyword.t()) :: value | {value, XSD.Datatype.uncanonical_lexical()} def elixir_mapping(value, opts) # Special case for date and dateTime, for which 0 is not a valid year def elixir_mapping(%Date{year: 0}, _), do: @invalid_value + def elixir_mapping({%Date{year: 0}, _}, _), do: @invalid_value def elixir_mapping(%Date{} = value, opts) do if tz = Keyword.get(opts, :tz) do - if valid_timezone?(tz) do - {{value, timezone_mapping(tz)}, nil} - else - @invalid_value - end + elixir_mapping({value, tz}, opts) else value end end + def elixir_mapping({%Date{} = value, tz}, _opts) when is_binary(tz) do + if valid_timezone?(tz) do + {{value, timezone_mapping(tz)}, nil} + else + @invalid_value + end + end + def elixir_mapping(_, _), do: @invalid_value defp valid_timezone?(string), do: Regex.match?(@tz_grammar, string) diff --git a/lib/rdf/xsd/datatypes/time.ex b/lib/rdf/xsd/datatypes/time.ex index f5f61ef..719b205 100644 --- a/lib/rdf/xsd/datatypes/time.ex +++ b/lib/rdf/xsd/datatypes/time.ex @@ -38,29 +38,21 @@ defmodule RDF.XSD.Time do @impl XSD.Datatype def lexical_mapping(lexical, opts) do case Regex.run(@grammar, lexical) do - [_, time] -> - do_lexical_mapping(time, opts) - - [_, time, tz] -> - do_lexical_mapping( - time, - opts |> Keyword.put_new(:tz, tz) |> Keyword.put_new(:lexical_present, true) - ) - - _ -> - @invalid_value + [_, time] -> do_lexical_mapping(time, nil, opts) + [_, time, tz] -> do_lexical_mapping(time, tz, opts) + _ -> @invalid_value end end - defp do_lexical_mapping(value, opts) do + defp do_lexical_mapping(value, tz, opts) do + do_lexical_mapping(value, Keyword.get(opts, :tz, tz)) + end + + defp do_lexical_mapping(value, tz) do case Time.from_iso8601(value) do - {:ok, time} -> elixir_mapping(time, opts) + {:ok, time} -> time_value(time, tz) _ -> @invalid_value end - |> case do - {{_, true} = value, _} -> value - value -> value - end end @impl XSD.Datatype @@ -70,20 +62,34 @@ defmodule RDF.XSD.Time do def elixir_mapping(%Time{} = value, opts) do if tz = Keyword.get(opts, :tz) do - case with_offset(value, tz) do - @invalid_value -> - @invalid_value - - time -> - {{time, true}, unless(Keyword.get(opts, :lexical_present), do: Time.to_iso8601(value))} - end + elixir_mapping({value, tz}, opts) else value end end + def elixir_mapping({%Time{} = time, tz}, _opts) do + case time_value(time, tz) do + @invalid_value -> @invalid_value + time_with_tz -> {time_with_tz, Time.to_iso8601(time) <> if(tz == true, do: "Z", else: tz)} + end + end + def elixir_mapping(_, _), do: @invalid_value + defp time_value(time, nil), do: time + defp time_value(time, false), do: time + defp time_value(time, true), do: {time, true} + + defp time_value(time, zone) when is_binary(zone) do + case with_offset(time, zone) do + @invalid_value -> @invalid_value + time -> {time, true} + end + end + + defp time_value(_, _), do: @invalid_value + defp with_offset(time, zone) when zone in ~W[Z UTC GMT], do: time defp with_offset(time, offset) do diff --git a/test/unit/xsd/datatypes/date_test.exs b/test/unit/xsd/datatypes/date_test.exs index 2c4263e..f90c112 100644 --- a/test/unit/xsd/datatypes/date_test.exs +++ b/test/unit/xsd/datatypes/date_test.exs @@ -14,6 +14,9 @@ defmodule RDF.XSD.DateTest do valid: %{ # input => { value, lexical, canonicalized } ~D[2010-01-01] => {~D[2010-01-01], nil, "2010-01-01"}, + {~D[2010-01-01], "Z"} => {{~D[2010-01-01], "Z"}, nil, "2010-01-01Z"}, + {~D[2010-01-01], "+01:00"} => {{~D[2010-01-01], "+01:00"}, nil, "2010-01-01+01:00"}, + {~D[2010-01-01], "+00:00"} => {{~D[2010-01-01], "Z"}, nil, "2010-01-01Z"}, "2010-01-01" => {~D[2010-01-01], nil, "2010-01-01"}, "2010-01-01Z" => {{~D[2010-01-01], "Z"}, nil, "2010-01-01Z"}, "2010-01-01+00:00" => {{~D[2010-01-01], "Z"}, "2010-01-01+00:00", "2010-01-01Z"}, @@ -40,8 +43,10 @@ defmodule RDF.XSD.DateTest do false, 2010, 3.14, - # this value representation is just internal and not accepted as - {~D[2010-01-01], "Z"} + {~D[2010-01-01], "01:00"}, + {~D[2010-01-01], true}, + {"2010-01-01", "Z"}, + {~D[0000-01-01], "Z"} ] describe "new/2" do diff --git a/test/unit/xsd/datatypes/time_test.exs b/test/unit/xsd/datatypes/time_test.exs index f9d2064..38073f0 100644 --- a/test/unit/xsd/datatypes/time_test.exs +++ b/test/unit/xsd/datatypes/time_test.exs @@ -15,6 +15,10 @@ defmodule RDF.XSD.TimeTest do # input => { value, lexical, canonicalized } ~T[00:00:00] => {~T[00:00:00], nil, "00:00:00"}, ~T[00:00:00.123] => {~T[00:00:00.123], nil, "00:00:00.123"}, + {~T[00:00:00], true} => {{~T[00:00:00], true}, nil, "00:00:00Z"}, + {~T[00:00:00], "Z"} => {{~T[00:00:00], true}, nil, "00:00:00Z"}, + {~T[01:00:00], "+01:00"} => {{~T[00:00:00], true}, "01:00:00+01:00", "00:00:00Z"}, + {~T[01:00:00], "+00:00"} => {{~T[01:00:00], true}, "01:00:00+00:00", "01:00:00Z"}, "00:00:00" => {~T[00:00:00], nil, "00:00:00"}, "00:00:00.123" => {~T[00:00:00.123], nil, "00:00:00.123"}, "00:00:00Z" => {{~T[00:00:00], true}, nil, "00:00:00Z"}, @@ -41,13 +45,13 @@ defmodule RDF.XSD.TimeTest do 3.14, "00:00:00Z foo", "foo 00:00:00Z", - # this value representation is just internal and not accepted as - {~T[00:00:00], true}, - {~T[00:00:00], "Z"} + {~T[00:00:00], "00:00"}, + {~T[00:00:00], 42}, + {"01:00:00", "+01:00"} ] describe "new/2" do - test "with date and tz opt" do + test "with time and tz opt" do assert XSD.Time.new("12:00:00", tz: "+01:00") == %RDF.Literal{ literal: %XSD.Time{