Skip to content
Merged
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
220 changes: 187 additions & 33 deletions lib/elixir/lib/calendar/iso.ex
Original file line number Diff line number Diff line change
Expand Up @@ -1220,6 +1220,29 @@ defmodule Calendar.ISO do
:basic | :extended
) :: String.t()
def time_to_string(
hour,
minute,
second,
microsecond,
format \\ :extended
) do
time_to_iodata(hour, minute, second, microsecond, format)
|> IO.iodata_to_binary()
end

@doc """
Converts the given time into a iodata.

Look at time_to_string/5 for more information
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Converts the given time into a iodata.
Look at time_to_string/5 for more information
Converts the given time into an iodata.
See `time_to_string/5` for more information.


## Examples

iex> Calendar.ISO.time_to_iodata(2, 2, 2, {2, 6})
[[["0", "2"], 58, ["0", "2"], 58, ["0", "2"]], 46, ["00000", "2"]]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if we should show the exact iodata, or a call to IO.iodata_to_binary() (which is what we usually do)


"""
@doc since: "1.19.0"
def time_to_iodata(
hour,
minute,
second,
Expand All @@ -1228,24 +1251,41 @@ defmodule Calendar.ISO do
)
when is_hour(hour) and is_minute(minute) and is_second(second) and
is_microsecond(ms_value, ms_precision) and format in [:basic, :extended] do
time_to_string_guarded(hour, minute, second, microsecond, format)
time_to_iodata_guarded(hour, minute, second, microsecond, format)
end

defp time_to_string_guarded(hour, minute, second, {_, 0}, format) do
time_to_string_format(hour, minute, second, format)
defp time_to_iodata_guarded(hour, minute, second, {_, 0}, format) do
time_to_iodata_format(hour, minute, second, format)
end

defp time_to_string_guarded(hour, minute, second, {microsecond, precision}, format) do
time_to_string_format(hour, minute, second, format) <>
"." <> (microsecond |> zero_pad(6) |> binary_part(0, precision))
defp time_to_iodata_guarded(hour, minute, second, {microsecond, precision}, format) do
[
time_to_iodata_format(hour, minute, second, format),
?.,
microseconds_to_iodata(microsecond, precision)
]
end

defp time_to_string_format(hour, minute, second, :extended) do
zero_pad(hour, 2) <> ":" <> zero_pad(minute, 2) <> ":" <> zero_pad(second, 2)
defp microseconds_to_iodata(_microsecond, 0), do: []
defp microseconds_to_iodata(microsecond, 6), do: zero_pad(microsecond, 6)

defp microseconds_to_iodata(microsecond, precision) do
num = div(microsecond, div_factor(precision))
zero_pad(num, precision)
end

defp time_to_string_format(hour, minute, second, :basic) do
zero_pad(hour, 2) <> zero_pad(minute, 2) <> zero_pad(second, 2)
defp div_factor(1), do: 100_000
defp div_factor(2), do: 10_000
defp div_factor(3), do: 1_000
defp div_factor(4), do: 100
defp div_factor(5), do: 10

defp time_to_iodata_format(hour, minute, second, :extended) do
[zero_pad(hour, 2), ?:, zero_pad(minute, 2), ?:, zero_pad(second, 2)]
end

defp time_to_iodata_format(hour, minute, second, :basic) do
[zero_pad(hour, 2), zero_pad(minute, 2), zero_pad(second, 2)]
end

@doc """
Expand Down Expand Up @@ -1273,18 +1313,34 @@ defmodule Calendar.ISO do
@doc since: "1.4.0"
@spec date_to_string(year, month, day, :basic | :extended) :: String.t()
@impl true
def date_to_string(year, month, day, format \\ :extended)
def date_to_string(year, month, day, format \\ :extended) do
date_to_iodata(year, month, day, format)
|> IO.iodata_to_binary()
end

@doc """
Converts the given date into a iodata.
Look at date_to_string/4 for more information
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Converts the given date into a iodata.
Look at date_to_string/4 for more information
Converts the given date into an iodata.
See `date_to_string/4` for more information.


## Examples

iex> Calendar.ISO.date_to_iodata(2015, 2, 28)
["2015", 45, ["0", "2"], 45, "28"]
"""
@doc since: "1.19.0"
@spec date_to_iodata(year, month, day, :basic | :extended) :: iodata
def date_to_iodata(year, month, day, format \\ :extended)
when is_integer(year) and is_integer(month) and is_integer(day) and
format in [:basic, :extended] do
date_to_string_guarded(year, month, day, format)
date_to_iodata_guarded(year, month, day, format)
end

defp date_to_string_guarded(year, month, day, :extended) do
zero_pad(year, 4) <> "-" <> zero_pad(month, 2) <> "-" <> zero_pad(day, 2)
defp date_to_iodata_guarded(year, month, day, :extended) do
[zero_pad(year, 4), ?-, zero_pad(month, 2), ?-, zero_pad(day, 2)]
end

defp date_to_string_guarded(year, month, day, :basic) do
zero_pad(year, 4) <> zero_pad(month, 2) <> zero_pad(day, 2)
defp date_to_iodata_guarded(year, month, day, :basic) do
[zero_pad(year, 4), zero_pad(month, 2), zero_pad(day, 2)]
end

@doc """
Expand Down Expand Up @@ -1327,8 +1383,45 @@ defmodule Calendar.ISO do
microsecond,
format \\ :extended
) do
date_to_string(year, month, day, format) <>
" " <> time_to_string(hour, minute, second, microsecond, format)
naive_datetime_to_iodata(
year,
month,
day,
hour,
minute,
second,
microsecond,
format
)
|> IO.iodata_to_binary()
end

@doc """
Converts the given naive_datetime into a iodata.
Look at naive_datetime_to_iodata/8 for more information
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Converts the given naive_datetime into a iodata.
Look at naive_datetime_to_iodata/8 for more information
Converts the given naive_datetime into an iodata.
See `naive_datetime_to_iodata/8` for more information.


## Examples

iex> Calendar.ISO.naive_datetime_to_iodata(2015, 2, 28, 1, 2, 3, {4, 6}, :basic)
[["2015", ["0", "2"], "28"], 32, [[["0", "1"], ["0", "2"], ["0", "3"]], 46, ["00000", "4"]]]

"""
@doc since: "1.19.0"
def naive_datetime_to_iodata(
year,
month,
day,
hour,
minute,
second,
microsecond,
format \\ :extended
) do
[
date_to_iodata(year, month, day, format),
?\s,
time_to_iodata(hour, minute, second, microsecond, format)
]
end

@doc """
Expand Down Expand Up @@ -1394,20 +1487,73 @@ defmodule Calendar.ISO do
utc_offset,
std_offset,
format \\ :extended
) do
datetime_to_iodata(
year,
month,
day,
hour,
minute,
second,
microsecond,
time_zone,
zone_abbr,
utc_offset,
std_offset,
format
)
|> IO.iodata_to_binary()
end

@doc """
Converts the given datetime into a iodata.
Look at datetime_to_iodata/12 for more information
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Converts the given datetime into a iodata.
Look at datetime_to_iodata/12 for more information
Converts the given datetime into an iodata.
See `datetime_to_iodata/12` for more information.


## Examples

iex> time_zone = "Etc/UTC"
iex> Calendar.ISO.datetime_to_iodata(2017, 8, 1, 1, 2, 3, {4, 5}, time_zone, "UTC", 0, 0)
[["2017", 45, ["0", "8"], 45, ["0", "1"]], 32, [[["0", "1"], 58, ["0", "2"], 58, ["0", "3"]], 46, ["0000", "0"]], 90, []]

"""
@doc since: "1.19.0"
def datetime_to_iodata(
year,
month,
day,
hour,
minute,
second,
microsecond,
time_zone,
zone_abbr,
utc_offset,
std_offset,
format \\ :extended
)
when is_time_zone(time_zone) and is_zone_abbr(zone_abbr) and is_utc_offset(utc_offset) and
is_std_offset(std_offset) do
date_to_string(year, month, day, format) <>
" " <>
time_to_string(hour, minute, second, microsecond, format) <>
offset_to_string(utc_offset, std_offset, time_zone, format) <>
zone_to_string(utc_offset, std_offset, zone_abbr, time_zone)
[
date_to_iodata(year, month, day, format),
?\s,
time_to_iodata(hour, minute, second, microsecond, format),
offset_to_iodata(utc_offset, std_offset, time_zone, format),
zone_to_iodata(utc_offset, std_offset, zone_abbr, time_zone)
]
end

@doc false
def offset_to_string(0, 0, "Etc/UTC", _format), do: "Z"

def offset_to_string(utc, std, _zone, format) do
def offset_to_string(utc, std, zone, format) do
offset_to_iodata(utc, std, zone, format)
|> IO.iodata_to_binary()
end

@doc false
def offset_to_iodata(0, 0, "Etc/UTC", _format), do: ?Z

def offset_to_iodata(utc, std, _zone, format) do
total = utc + std
second = abs(total)
minute = second |> rem(3600) |> div(60)
Expand All @@ -1416,15 +1562,15 @@ defmodule Calendar.ISO do
end

defp format_offset(total, hour, minute, :extended) do
sign(total) <> zero_pad(hour, 2) <> ":" <> zero_pad(minute, 2)
[sign(total), zero_pad(hour, 2), ?:, zero_pad(minute, 2)]
end

defp format_offset(total, hour, minute, :basic) do
sign(total) <> zero_pad(hour, 2) <> zero_pad(minute, 2)
[sign(total), zero_pad(hour, 2), zero_pad(minute, 2)]
end

defp zone_to_string(_, _, _, "Etc/UTC"), do: ""
defp zone_to_string(_, _, abbr, zone), do: " " <> abbr <> " " <> zone
defp zone_to_iodata(_, _, _, "Etc/UTC"), do: []
defp zone_to_iodata(_, _, abbr, zone), do: [?\s, abbr, ?\s, zone]

@doc """
Determines if the date given is valid according to the proleptic Gregorian calendar.
Expand Down Expand Up @@ -1485,16 +1631,24 @@ defmodule Calendar.ISO do
{0, 1}
end

defp sign(total) when total < 0, do: "-"
defp sign(_), do: "+"
defp sign(total) when total < 0, do: ?-
defp sign(_), do: ?+

defp zero_pad(val, count) when val >= 0 do
defp zero_pad(val, count) when val >= 0 and count <= 6 do
num = Integer.to_string(val)
:binary.copy("0", max(count - byte_size(num), 0)) <> num

case max(count - byte_size(num), 0) do
0 -> num
1 -> ["0", num]
2 -> ["00", num]
3 -> ["000", num]
4 -> ["0000", num]
5 -> ["00000", num]
end
end

defp zero_pad(val, count) do
"-" <> zero_pad(-val, count)
[?-, zero_pad(-val, count)]
Copy link
Contributor

@sabiwara sabiwara Dec 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps a further optimization if we're using IO-data is to use improper lists when possible, replacing the last , by |:

Suggested change
[?-, zero_pad(-val, count)]
[?- | zero_pad(-val, count)]

I haven't benchmarked it and wouldn't expect it to be a huge difference, but it should create less cons cells in theory?
(we might need to use @dialyzer :no_improper_lists)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sabiwara it makes a difference in the memory usage:

Name                                        ips        average  deviation         median         99th %
Calendar.IOISO.datetime_to_iodata        4.89 M      204.42 ns ±19386.20%         160 ns         271 ns

Memory usage statistics:

Name                                 Memory usage
Calendar.IOISO.datetime_to_iodata           376 B

**All measurements for memory usage were the same**

end

@doc """
Expand Down
Loading