Skip to content

Commit

Permalink
Return error tuple immediate if a format can't be fulfilled
Browse files Browse the repository at this point in the history
  • Loading branch information
kipcole9 committed Aug 31, 2019
1 parent b1bf24b commit 9776011
Show file tree
Hide file tree
Showing 10 changed files with 151 additions and 58 deletions.
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
# Changelog for Cldr_Dates_Times v2.2.2

This is the changelog for Cldr_Dates_Times v2.2.2 released on August 31st, 2019. For older changelogs please consult the release tag on [GitHub](https://github.com/elixir-cldr/cldr_dates_times/tags)

### Changes & Deprecations

* Deprecates the option `:format` on `Cldr.DateTime.Relative.to_string/3` in favour of `:style`. `:format` will be removed with `ex_cldr_dates_times` version 3.0

### Bug Fixes

* Return an error tuple immediately when a format code is used but no data is available to fulfill it

# Changelog for Cldr_Dates_Times v2.2.1

This is the changelog for Cldr_Dates_Times v2.2.1 released on August 23rd, 2019. For older changelogs please consult the release tag on [GitHub](https://github.com/elixir-cldr/cldr_dates_times/tags)
Expand Down
13 changes: 11 additions & 2 deletions lib/cldr/date.ex
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,8 @@ defmodule Cldr.Date do
{:ok, "10 Julie 2017"}
"""
@spec to_string(map, Cldr.backend() | Keyword.t(), Keyword.t()) :: {:ok, String.t()} | {:error, {module, String.t()}}
@spec to_string(map, Cldr.backend() | Keyword.t(), Keyword.t()) ::
{:ok, String.t()} | {:error, {module, String.t()}}

def to_string(date, backend \\ Cldr.default_backend(), options \\ [])

Expand All @@ -103,6 +104,9 @@ defmodule Cldr.Date do
else
{:error, reason} -> {:error, reason}
end
rescue
e in [Cldr.DateTime.UnresolvedFormat] ->
{:error, {e.__struct__, e.message}}
end

def to_string(date, _backend, _options) do
Expand Down Expand Up @@ -200,9 +204,14 @@ defmodule Cldr.Date do
end

defp error_return(map, requirements) do
requirements =
requirements
|> Enum.map(&inspect/1)
|> Cldr.DateTime.Formatter.join_requirements()

{:error,
{ArgumentError,
"Invalid date. Date is a map that requires at least #{inspect(requirements)} fields. " <>
"Invalid date. Date is a map that contains at least #{requirements}. " <>
"Found: #{inspect(map)}"}}
end
end
15 changes: 11 additions & 4 deletions lib/cldr/datetime.ex
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@ defmodule Cldr.DateTime do
{:ok, "samedi 1 janvier 2000 à 23:59:59 UTC"}
"""
@spec to_string(map, Cldr.backend() | Keyword.t(), Keyword.t()) :: {:ok, String.t()} | {:error, {module, String.t()}}
@spec to_string(map, Cldr.backend() | Keyword.t(), Keyword.t()) ::
{:ok, String.t()} | {:error, {module, String.t()}}

def to_string(datetime, backend \\ Cldr.default_backend(), options \\ [])

Expand All @@ -106,9 +107,10 @@ defmodule Cldr.DateTime do
{:ok, format_string} <- format_string(options[:format], locale, cldr_calendar, backend),
{:ok, formatted} <- format_backend.format(datetime, format_string, locale, options) do
{:ok, formatted}
else
{:error, reason} -> {:error, reason}
end
rescue
e in [Cldr.DateTime.UnresolvedFormat] ->
{:error, {e.__struct__, e.message}}
end

def to_string(datetime, _backend, _options) do
Expand Down Expand Up @@ -230,9 +232,14 @@ defmodule Cldr.DateTime do
end

defp error_return(map, requirements) do
requirements =
requirements
|> Enum.map(&inspect/1)
|> Cldr.DateTime.Formatter.join_requirements()

{:error,
{ArgumentError,
"Invalid date_time. Date_time is a map that requires at least #{inspect(requirements)} fields. " <>
"Invalid DateTime. DateTime is a map that contains at least #{requirements}. " <>
"Found: #{inspect(map)}"}}
end
end
12 changes: 12 additions & 0 deletions lib/cldr/datetime/exception.ex
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,15 @@ defmodule Cldr.DateTime.Compiler.ParseError do
%__MODULE__{message: message}
end
end

defmodule Cldr.DateTime.UnresolvedFormat do
@moduledoc """
Exception raised when formatting and there is no
data for the given format.
"""
defexception [:message]

def exception(message) do
%__MODULE__{message: message}
end
end
78 changes: 47 additions & 31 deletions lib/cldr/datetime/relative.ex
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ defmodule Cldr.DateTime.Relative do

@other_units [:mon, :tue, :wed, :thu, :fri, :sat, :sun, :quarter]
@unit_keys Map.keys(@unit) ++ @other_units
@known_formats [:default, :narrow, :short]
@known_styles [:default, :narrow, :short]

@doc """
Returns a `{:ok, string}` representing a relative time (ago, in) for a given
Expand All @@ -47,7 +47,7 @@ defmodule Cldr.DateTime.Relative do
* `:locale` is the locale in which the binary is formatted.
The default is `Cldr.get_locale/0`
* `:format` is the format of the binary. Format may be `:default`, `:narrow` or `:short`
* `:style` is the style of the binary. Style may be `:default`, `:narrow` or `:short`
* `:unit` is the time unit for the formatting. The allowable units are `:second`, `:minute`,
`:hour`, `:day`, `:week`, `:month`, `:year`, `:mon`, `:tue`, `:wed`, `:thu`, `:fri`, `:sat`,
Expand Down Expand Up @@ -77,7 +77,7 @@ defmodule Cldr.DateTime.Relative do
iex> Cldr.DateTime.Relative.to_string(1, MyApp.Cldr, unit: :day, locale: "fr")
{:ok, "demain"}
iex> Cldr.DateTime.Relative.to_string(1, MyApp.Cldr, unit: :day, format: :narrow)
iex> Cldr.DateTime.Relative.to_string(1, MyApp.Cldr, unit: :day, style: :narrow)
{:ok, "tomorrow"}
iex> Cldr.DateTime.Relative.to_string(1234, MyApp.Cldr, unit: :year)
Expand All @@ -92,19 +92,19 @@ defmodule Cldr.DateTime.Relative do
iex> Cldr.DateTime.Relative.to_string(~D[2017-04-29], MyApp.Cldr, relative_to: ~D[2017-04-26])
{:ok, "in 3 days"}
iex> Cldr.DateTime.Relative.to_string(310, MyApp.Cldr, format: :short, locale: "fr")
iex> Cldr.DateTime.Relative.to_string(310, MyApp.Cldr, style: :short, locale: "fr")
{:ok, "dans 5 min"}
iex> Cldr.DateTime.Relative.to_string(310, MyApp.Cldr, format: :narrow, locale: "fr")
iex> Cldr.DateTime.Relative.to_string(310, MyApp.Cldr, style: :narrow, locale: "fr")
{:ok, "+5 min"}
iex> Cldr.DateTime.Relative.to_string 2, MyApp.Cldr, unit: :wed, format: :short, locale: "en"
iex> Cldr.DateTime.Relative.to_string 2, MyApp.Cldr, unit: :wed, style: :short, locale: "en"
{:ok, "in 2 Wed."}
iex> Cldr.DateTime.Relative.to_string 1, MyApp.Cldr, unit: :wed, format: :short
iex> Cldr.DateTime.Relative.to_string 1, MyApp.Cldr, unit: :wed, style: :short
{:ok, "next Wed."}
iex> Cldr.DateTime.Relative.to_string -1, MyApp.Cldr, unit: :wed, format: :short
iex> Cldr.DateTime.Relative.to_string -1, MyApp.Cldr, unit: :wed, style: :short
{:ok, "last Wed."}
iex> Cldr.DateTime.Relative.to_string -1, MyApp.Cldr, unit: :wed
Expand All @@ -121,6 +121,9 @@ defmodule Cldr.DateTime.Relative do
"Unknown time unit :ziggeraut. Valid time units are [:day, :hour, :minute, :month, :second, :week, :year, :mon, :tue, :wed, :thu, :fri, :sat, :sun, :quarter]"}}
"""

# TODO deprecate the option :format in favour of using :style in version 3.9

@spec to_string(integer | float | Date.t() | DateTime.t(), Cldr.backend(), Keyword.t()) ::
{:ok, String.t()} | {:error, {module, String.t()}}

Expand All @@ -137,23 +140,25 @@ defmodule Cldr.DateTime.Relative do

with {:ok, locale} <- Cldr.validate_locale(locale),
{:ok, unit} <- validate_unit(unit),
{:ok, _format} <- validate_format(options[:format]) do
{:ok, _style} <- validate_style(options[:style] || options[:format]) do
{relative, unit} = define_unit_and_relative_time(relative, unit, options[:relative_to])
string = to_string(relative, unit, locale, backend, options)
{:ok, string}
end
end

defp default_options do
[locale: Cldr.get_locale(), format: :default]
[locale: Cldr.get_locale(), style: :default]
end

# No unit or relative_to is specified so we derive them
defp define_unit_and_relative_time(relative, nil, nil) when is_number(relative) do
unit = unit_from_relative_time(relative)
relative = scale_relative(relative, unit)
{relative, unit}
end

# Take two datetimes and calculate the seconds between them
defp define_unit_and_relative_time(
%{year: _, month: _, day: _, hour: _, minute: _, second: _, calendar: Calendar.ISO} =
relative,
Expand All @@ -166,6 +171,7 @@ defmodule Cldr.DateTime.Relative do
define_unit_and_relative_time(relative_time, unit, nil)
end

# Take two dates and calculate the days between them
defp define_unit_and_relative_time(
%{year: _, month: _, day: _, calendar: Calendar.ISO} = relative,
unit,
Expand All @@ -187,6 +193,7 @@ defmodule Cldr.DateTime.Relative do
define_unit_and_relative_time(relative_time, unit, nil)
end

# Anything else just return the values
defp define_unit_and_relative_time(relative_time, unit, _relative_to) do
{relative_time, unit}
end
Expand All @@ -210,7 +217,7 @@ defmodule Cldr.DateTime.Relative do
* `:locale` is the locale in which the binary is formatted.
The default is `Cldr.get_locale/0`
* `:format` is the format of the binary. Format may be `:default`, `:narrow` or `:short`.
* `:style` is the format of the binary. Style may be `:default`, `:narrow` or `:short`.
The default is `:default`
* `:unit` is the time unit for the formatting. The allowable units are `:second`, `:minute`,
Expand Down Expand Up @@ -240,26 +247,34 @@ defmodule Cldr.DateTime.Relative do

@spec to_string(integer | float, atom(), Cldr.LanguageTag.t(), Cldr.backend(), Keyword.t()) ::
String.t()

defp to_string(relative, unit, locale, backend, options)

# For the case when its relative by one unit, for example "tomorrow" or "yesterday"
# or "last"
defp to_string(relative, unit, locale, backend, options) when relative in -1..1 do
style = options[:style] || options[:format]

result =
locale
|> get_locale(backend)
|> get_in([unit, options[:format], :relative_ordinal])
|> get_in([unit, style, :relative_ordinal])
|> Enum.at(relative + 1)

if is_nil(result), do: to_string(relative / 1, unit, locale, backend, options), else: result
end

# For the case when its more than one unit away. For example, "in 3 days"
# or "2 days ago"
defp to_string(relative, unit, locale, backend, options)
when is_float(relative) or is_integer(relative) do
direction = if relative > 0, do: :relative_future, else: :relative_past
style = options[:style] || options[:format]

rules =
locale
|> get_locale(backend)
|> get_in([unit, options[:format], direction])
|> get_in([unit, style, direction])

rule = Module.concat(backend, Number.Cardinal).pluralize(trunc(relative), locale, rules)

Expand All @@ -270,24 +285,25 @@ defmodule Cldr.DateTime.Relative do
|> Enum.join()
end

defp to_string(span, unit, locale, backend, options) do
do_to_string(span, unit, locale, backend, options)
end

defp do_to_string(seconds, unit, locale, backend, options) do
seconds
|> scale_relative(unit)
|> to_string(unit, locale, backend, options)
end
# For all other cases
# defp to_string(span, unit, locale, backend, options) do
# do_to_string(span, unit, locale, backend, options)
# end
#
# defp do_to_string(seconds, unit, locale, backend, options) do
# seconds
# |> scale_relative(unit)
# |> to_string(unit, locale, backend, options)
# end

defp time_unit_error(unit) do
{Cldr.UnknownTimeUnit,
"Unknown time unit #{inspect(unit)}. Valid time units are #{inspect(@unit_keys)}"}
end

defp format_error(format) do
{Cldr.UnknownFormatError,
"Unknown format #{inspect(format)}. Valid formats are #{inspect(@known_formats)}"}
defp style_error(style) do
{Cldr.UnknownStyleError,
"Unknown style #{inspect(style)}. Valid styles are #{inspect(@known_styles)}"}
end

@doc """
Expand Down Expand Up @@ -374,16 +390,16 @@ defmodule Cldr.DateTime.Relative do
{:error, time_unit_error(unit)}
end

def known_formats do
@known_formats
def known_styles do
@known_styles
end

defp validate_format(format) when format in @known_formats do
{:ok, format}
defp validate_style(style) when style in @known_styles do
{:ok, style}
end

defp validate_format(format) do
{:error, format_error(format)}
defp validate_style(style) do
{:error, style_error(style)}
end

defp get_locale(locale, backend) do
Expand Down
30 changes: 26 additions & 4 deletions lib/cldr/format/datetime_formatter.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3339,9 +3339,31 @@ defmodule Cldr.DateTime.Formatter do
defp number_of_digits(n), do: Enum.count(Integer.digits(n))

defp error_return(map, symbol, requirements) do
{:error,
"The format symbol '#{symbol}' requires at least #{inspect(requirements)}. Found: #{
inspect(map)
}"}
requirements =
requirements
|> Enum.map(&inspect/1)
|> join_requirements

raise Cldr.DateTime.UnresolvedFormat,
"The format symbol '#{symbol}' requires at map with at least #{requirements}. Found: #{
inspect(map)
}"
end

@doc false
def join_requirements([]) do
""
end

def join_requirements([head]) do
head
end

def join_requirements([head, tail]) do
"#{head} and #{tail}"
end

def join_requirements([head | tail]) do
to_string(head) <> ", " <> join_requirements(tail)
end
end
15 changes: 11 additions & 4 deletions lib/cldr/time.ex
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@ defmodule Cldr.Time do
{:ok, "11:59:59 PM UTC"}
"""
@spec to_string(map, module() | Keyword.t(), Keyword.t()) :: {:ok, String.t()} | {:error, {module, String.t()}}
@spec to_string(map, module() | Keyword.t(), Keyword.t()) ::
{:ok, String.t()} | {:error, {module, String.t()}}

def to_string(time, backend \\ Cldr.default_backend(), options \\ [])

Expand All @@ -106,9 +107,10 @@ defmodule Cldr.Time do
{:ok, format_string} <- format_string(options[:format], locale, cldr_calendar, backend),
{:ok, formatted} <- format_backend.format(time, format_string, locale, options) do
{:ok, formatted}
else
{:error, reason} -> {:error, reason}
end
rescue
e in [Cldr.DateTime.UnresolvedFormat] ->
{:error, {e.__struct__, e.message}}
end

def to_string(time, _backend, _options) do
Expand Down Expand Up @@ -218,9 +220,14 @@ defmodule Cldr.Time do
end

defp error_return(map, requirements) do
requirements =
requirements
|> Enum.map(&inspect/1)
|> Cldr.DateTime.Formatter.join_requirements()

{:error,
{ArgumentError,
"Invalid time. Time is a map that requires at least #{inspect(requirements)} fields. " <>
"Invalid time. Time is a map that contains at least #{requirements} fields. " <>
"Found: #{inspect(map)}"}}
end
end
Loading

0 comments on commit 9776011

Please sign in to comment.