diff --git a/lib/date/date.ex b/lib/date/date.ex index c9a78306..f3f14964 100644 --- a/lib/date/date.ex +++ b/lib/date/date.ex @@ -108,6 +108,16 @@ defimpl Timex.Protocol, for: Date do def day(%Date{} = date), do: 1 + Timex.diff(date, %Date{:year => date.year, :month => 1, :day => 1}, :days) + def ordinal_suffix(%Date{} = date) do + value = Integer.mod(date.day, 10) + cond do + value == 1 && date.day != 11 -> "st" + value == 2 && date.day != 12 -> "nd" + value == 3 && date.day != 13 -> "rd" + true -> "th" + end + end + def is_valid?(%Date{:year => y, :month => m, :day => d}) do :calendar.valid_date({y,m,d}) end diff --git a/lib/datetime/datetime.ex b/lib/datetime/datetime.ex index 1f8a4050..8f7bd946 100644 --- a/lib/datetime/datetime.ex +++ b/lib/datetime/datetime.ex @@ -145,6 +145,16 @@ defimpl Timex.Protocol, for: DateTime do 1 + Timex.diff(date, ref, :days) end + def ordinal_suffix(%DateTime{} = date) do + value = Integer.mod(date.day, 10) + cond do + value == 1 && date.day != 11 -> "st" + value == 2 && date.day != 12 -> "nd" + value == 3 && date.day != 13 -> "rd" + true -> "th" + end + end + def is_valid?(%DateTime{:year => y, :month => m, :day => d, :hour => h, :minute => min, :second => sec}) do :calendar.valid_date({y,m,d}) and Timex.is_valid_time?({h,min,sec}) diff --git a/lib/datetime/erlang.ex b/lib/datetime/erlang.ex index ba4a06bf..b5da944b 100644 --- a/lib/datetime/erlang.ex +++ b/lib/datetime/erlang.ex @@ -199,6 +199,26 @@ defimpl Timex.Protocol, for: Tuple do do: 1 + Timex.diff(date, {y,1,1}, :days) def day(_), do: {:error, :invalid_date} + def ordinal_suffix({y,m,d} = date) when is_date(y,m,d) do + value = Integer.mod(date.day, 10) + cond do + value == 1 && date.day != 11 -> "st" + value == 2 && date.day != 12 -> "nd" + value == 3 && date.day != 13 -> "rd" + true -> "th" + end + end + def ordinal_suffix({{y,m,d} = date,_}) when is_date(y,m,d) do + value = Integer.mod(date.day, 10) + cond do + value == 1 && date.day != 11 -> "st" + value == 2 && date.day != 12 -> "nd" + value == 3 && date.day != 13 -> "rd" + true -> "th" + end + end + def ordinal_suffix(_), do: {:error, :invalid_date} + def is_valid?({y,m,d}) when is_date(y,m,d), do: true def is_valid?({{y,m,d},{h,mm,s}}) when is_datetime(y,m,d,h,mm,s), do: true def is_valid?({{y,m,d},{h,mm,s,_us}}) when is_datetime(y,m,d,h,mm,s), do: true diff --git a/lib/datetime/map.ex b/lib/datetime/map.ex index 46165f5c..b9d11244 100644 --- a/lib/datetime/map.ex +++ b/lib/datetime/map.ex @@ -53,6 +53,7 @@ defimpl Timex.Protocol, for: Map do def week_of_month(map), do: convert!(map, :week_of_month) def weekday(map), do: convert!(map, :weekday) def day(map), do: convert!(map, :day) + def ordinal_suffix(map), do: convert!(map, :ordinal_suffix) def is_valid?(map), do: convert!(map, :is_valid?) def iso_week(map), do: convert!(map, :iso_week) def from_iso_day(map, day), do: convert(map, :from_iso_day, [day]) diff --git a/lib/datetime/naivedatetime.ex b/lib/datetime/naivedatetime.ex index 26d4f684..af667d26 100644 --- a/lib/datetime/naivedatetime.ex +++ b/lib/datetime/naivedatetime.ex @@ -122,6 +122,16 @@ defimpl Timex.Protocol, for: NaiveDateTime do 1 + Timex.diff(date, nd, :days) end + def ordinal_suffix(%NaiveDateTime{} = date) do + value = Integer.mod(date.day, 10) + cond do + value == 1 && date.day != 11 -> "st" + value == 2 && date.day != 12 -> "nd" + value == 3 && date.day != 13 -> "rd" + true -> "th" + end + end + def is_valid?(%NaiveDateTime{:year => y, :month => m, :day => d, :hour => h, :minute => min, :second => sec}) do :calendar.valid_date({y,m,d}) and Timex.is_valid_time?({h,min,sec}) diff --git a/lib/format/datetime/formatter.ex b/lib/format/datetime/formatter.ex index aa850192..f5a5312b 100644 --- a/lib/format/datetime/formatter.ex +++ b/lib/format/datetime/formatter.ex @@ -476,6 +476,8 @@ defmodule Timex.Format.DateTime.Formatter do do: pad_numeric(date.day, flags, width) def format_token(_locale, :oday, date, _modifiers, flags, width), do: pad_numeric(Timex.day(date), flags, width) + def format_token(_locale, :suffix, date, _modifiers, flags, width), + do: pad_numeric(Timex.ordinal_suffix(date), flags, width) # Weeks def format_token(_locale, :iso_weeknum, date, _modifiers, flags, width) do {_, week} = Timex.iso_week(date) diff --git a/lib/parse/datetime/tokenizers/default.ex b/lib/parse/datetime/tokenizers/default.ex index 78ec8cc6..b2af643c 100644 --- a/lib/parse/datetime/tokenizers/default.ex +++ b/lib/parse/datetime/tokenizers/default.ex @@ -41,7 +41,7 @@ defmodule Timex.Parse.DateTime.Tokenizers.Default do # Months "Mshort", "Mfull", "M", # Days - "Dord", "D", + "Dord", "D", "S", # Weeks "Wiso", "Wmon", "Wsun", "WDmon", "WDsun", "WDshort", "WDfull", # Time @@ -98,6 +98,7 @@ defmodule Timex.Parse.DateTime.Tokenizers.Default do # Days "D" -> set_width(1, 2, :day, directive, opts) "Dord" -> set_width(1, 3, :oday, directive, opts) + "S" -> force_width(2, :suffix, directive, opts) # Weeks "Wiso" -> force_width(2, :iso_weeknum, directive, opts) "Wmon" -> set_width(1, 2, :week_mon, directive, opts) diff --git a/lib/parse/datetime/tokenizers/directive.ex b/lib/parse/datetime/tokenizers/directive.ex index eef16b08..90047a00 100644 --- a/lib/parse/datetime/tokenizers/directive.ex +++ b/lib/parse/datetime/tokenizers/directive.ex @@ -42,7 +42,7 @@ defmodule Timex.Parse.DateTime.Tokenizers.Directive do ] @mapped_types [iso_year4: :year4, iso_year2: :year2, month: :month2, mshort: :month_short, mfull: :month_full, - day: :day_of_month, oday: :day_of_year, + day: :day_of_month, oday: :day_of_year, suffix: :day_of_month, iso_weeknum: :week_of_year, week_mon: :week_of_year, week_sun: :week_of_year_sun, wday_mon: :weekday, wday_sun: :weekday, wdshort: :weekday_short, wdfull: :weekday_full, min: :minute, sec: :second, sec_fractional: :second_fractional, sec_epoch: :seconds_epoch, diff --git a/lib/protocol.ex b/lib/protocol.ex index 4a3ae274..a901389c 100644 --- a/lib/protocol.ex +++ b/lib/protocol.ex @@ -178,6 +178,12 @@ defprotocol Timex.Protocol do @spec day(Types.valid_datetime) :: Types.daynum | {:error, term} def day(datetime) + @doc """ + Get the English ordinal suffix (st, nd, rd, th) of the given date/time value + """ + @spec ordinal_suffix(Types.valid_datetime) :: Types.daynum | {:error, term} + def ordinal_suffix(datetime) + @doc """ Determine if the provided date/time value is valid. """ @@ -280,6 +286,9 @@ defimpl Timex.Protocol, for: Any do def day(%{__struct__: _} = d), do: Timex.day(Map.from_struct(d)) def day(_datetime), do: {:error, :invalid_date} + def ordinal_suffix(%{__struct__: _} = d), do: Timex.ordinal_suffix(Map.from_struct(d)) + def ordinal_suffix(_datetime), do: {:error, :invalid_date} + def is_valid?(%{__struct__: _} = d), do: Timex.is_valid?(Map.from_struct(d)) def is_valid?(_datetime), do: false diff --git a/lib/timex.ex b/lib/timex.ex index 3d943be9..ea8f916e 100644 --- a/lib/timex.ex +++ b/lib/timex.ex @@ -1210,6 +1210,17 @@ defmodule Timex do @spec day(Types.valid_datetime()) :: Types.daynum() | {:error, term} defdelegate day(datetime), to: Timex.Protocol + @doc """ + Returns the English ordinal suffix (st, nd, rd, th) for the day. + + ## Examples + + iex> Timex.ordinal_suffix(~D[2015-06-26]) + th + """ + @spec ordinal_suffix(Types.valid_datetime()) :: Types.daynum() | {:error, term} + defdelegate ordinal_suffix(datetime), to: Timex.Protocol + @doc """ Return the number of days in the month which the date falls on.