BenFenner
Imperative cascading conditionals with short-circuit returns. Declarative approach?
I’m sorry for asking my 3rd question in a week, but I’m making so much progress I’d hate to slow down now.
I have a function (in PHP, sorry) that I need to convert the behavior of to Elixir. The function comment explains the purpose and behavior quite well:
// Take a number of seconds and return a string describing the length of time
// in the most appropriate way. If we're dealing with seconds on the order of
// hours, then return hours. If we're dealing with seconds on the order of
// years, then return years. You get the idea.
// This function takes leap years into account by treating one year as one
// year plus one forth of a day.
function display_seconds($seconds) {
if ($seconds > 94672800) { // 94,672,800 seconds is 3 years.
$seconds_in_one_year = 31557600; // 31,557,600 seconds is 1 year.
return number_format($seconds / $seconds_in_one_year) . ' years';
} elseif ($seconds > 7889400) { // 7,889,400 seconds is 3 months.
$seconds_in_one_month = 2629800; // 2,629,800 seconds is 1 month.
return number_format($seconds / $seconds_in_one_month) . ' months';
} elseif ($seconds > 1814400) { // 1,814,400 seconds is 3 weeks.
$seconds_in_one_week = 604800; // 604,800 seconds is 1 week.
return number_format($seconds / $seconds_in_one_week) . ' weeks';
} elseif ($seconds > 259200) { // 259,200 seconds is 3 days.
$seconds_in_one_day = 86400; // 86,400 seconds is 1 day.
return number_format($seconds / $seconds_in_one_day) . ' days';
} elseif ($seconds > 10800) { // 10,800 seconds is 3 hours.
$seconds_in_one_hour = 3600; // 3,600 seconds is 1 hour.
return number_format($seconds / $seconds_in_one_hour) . ' hours';
} elseif ($seconds > 90) { // 90 seconds is 3 minutes.
$seconds_in_one_minute = 60; // 60 seconds is 1 minute.
return number_format($seconds / $seconds_in_one_minute) . ' minutes';
} else {
return number_format($seconds) . ' seconds';
}
}
If you followed along, you’ll see that the function takes an amount of seconds, and turns that into a human-friendly string, for example 6 weeks or 13 years. Id’ like to accomplish the same thing in Elixir. I’ve already got the number_format() part figured out. I can use Number.Delimit.number_to_delimited() for that. It’s the if/elseif/else logic and the short-circuit returns that are my main issue. No doubt there is an entirely different approach I should be taking here, I just don’t know what it is. Or maybe even this type of time/string formatting is built into the language?
I’d appreciate any help I can get. I think I could probably adapt the answer to my previous question to accomplish something like this, but I’m not confident there either…
Marked As Solved
kokolegorille
This is an excellent example of how useful pattern matching is…
You can solve it with case do end, but You can also use something like this.
defmodule Demo do
@seconds_in_one_year 31_557_600
@seconds_in_one_month 2_629_800
def display_seconds(seconds) when seconds > 94_672_800, do: number_format(seconds / @seconds_in_one_year)
def display_seconds(seconds) when seconds > 7_889_400, do: number_format(seconds / @seconds_in_one_month)
defp number_format(number), do: "whatever #{number}"
end
Also Liked
BenFenner
I went with the pattern matching capability of functions as suggested, and ended up with this which works great. ![]()
I’m not super happy with the do/end on the last line. Is that normally how people do the final function?
@seconds_in_one_year 31557600 # 31,557,600 seconds | Includes leap years.
@seconds_in_one_month 2629800 # 2,629,800 seconds | Includes leap years.
@seconds_in_one_week 604800 # 604,800 seconds | Does not include leap years.
@seconds_in_one_day 86400 # 86,400 seconds | Does not include leap years.
@seconds_in_one_hour 3600 # 3,600 seconds | Does not include leap years.
@seconds_in_one_minute 60 # 60 seconds | Does not include leap years.
#
# Take a number of seconds and return a string describing the length of time
# in the most appropriate way. If we're dealing with seconds on the order of
# hours, then return hours. If we're dealing with seconds on the order of
# years, then return years. You get the idea.
# This function takes leap years into account by treating one year as one
# year plus one forth of a day.
#
defp display_seconds(seconds) when seconds > (3 * @seconds_in_one_year), do: Number.Delimit.number_to_delimited(seconds / @seconds_in_one_year, precision: 0) <> " years"
defp display_seconds(seconds) when seconds > (3 * @seconds_in_one_month), do: Number.Delimit.number_to_delimited(seconds / @seconds_in_one_month, precision: 0) <> " months"
defp display_seconds(seconds) when seconds > (3 * @seconds_in_one_week), do: Number.Delimit.number_to_delimited(seconds / @seconds_in_one_week, precision: 0) <> " weeks"
defp display_seconds(seconds) when seconds > (3 * @seconds_in_one_day), do: Number.Delimit.number_to_delimited(seconds / @seconds_in_one_day, precision: 0) <> " days"
defp display_seconds(seconds) when seconds > (3 * @seconds_in_one_hour), do: Number.Delimit.number_to_delimited(seconds / @seconds_in_one_hour, precision: 0) <> " hours"
defp display_seconds(seconds) when seconds > (3 * @seconds_in_one_minute), do: Number.Delimit.number_to_delimited(seconds / @seconds_in_one_minute, precision: 0) <> " minutes"
defp display_seconds(seconds) do Number.Delimit.number_to_delimited(seconds, precision: 0) <> " seconds" end
OvermindDL1
Why not just do , <lots of spaces> do: with no end? Personally I opt for putting ,do: as I like the alignment better.
Honestly I’d actually rather do this for the whole thing:
@seconds_in_one_year 31557600 # 31,557,600 seconds | Includes leap years.
@seconds_in_one_month 2629800 # 2,629,800 seconds | Includes leap years.
@seconds_in_one_week 604800 # 604,800 seconds | Does not include leap years.
@seconds_in_one_day 86400 # 86,400 seconds | Does not include leap years.
@seconds_in_one_hour 3600 # 3,600 seconds | Does not include leap years.
@seconds_in_one_minute 60 # 60 seconds | Does not include leap years.
@doc ~S"""
Take a number of seconds and return a string describing the length of time
in the most appropriate way. If we're dealing with seconds on the order of
hours, then return hours. If we're dealing with seconds on the order of
years, then return years. You get the idea.
This function takes leap years into account by treating one year as one
year plus one forth of a day.
"""
defp display_seconds(seconds), do: cond do
seconds > (3 * @seconds_in_one_year) -> {@seconds_in_one_year, "years"}
seconds > (3 * @seconds_in_one_month) -> {@seconds_in_one_month, "months"}
seconds > (3 * @seconds_in_one_week) -> {@seconds_in_one_week, "weeks"}
seconds > (3 * @seconds_in_one_day) -> {@seconds_in_one_day, "days"}
seconds > (3 * @seconds_in_one_hour) -> {@seconds_in_one_hour, "hours"}
seconds > (3 * @seconds_in_one_minute) -> {@seconds_in_one_minutes, "minutes"}
1 -> {1, "second"}
true -> {1, "seconds"}
end |> case do
{o, s} -> "#{Number.Delimit.number_to_delimited(seconds / o, precision: 0)} #{s}"
end
The formatter would of course destroy the formatting, so I’d break it up in to more functions, one that probably does the when guard checks and another to do the actual formatting via number_to_delimited, which is more readable anyway. ![]()
amarraja
Here’s a another approach. This removes the need to duplicate the constant names and comparison logic.
Note: I haven’t run this, and my names could do with a little more thought ![]()
@periods [
{31_557_600, "years"},
{2_629_800, "months"},
{604_800, "weeks"},
{86400, "days"},
{3600, "hours"},
{60, "minutes"}
]
defp display_seconds(seconds) do
{multiplier, unit} =
@periods
|> Enum.find(
{1, "seconds"}, #This is the default if it can't find anything
fn {seconds_in, _} -> seconds > 3 * seconds_in end
)
delimited = Number.Delimit.number_to_delimited(seconds / multiplier, precision: 0)
"#{delimited} #{unit}"
end
Popular in Questions
Other popular topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #elixirconf-us
- #advent-of-code
- #distillery
- #processes
- #forms
- #api
- #metaprogramming
- #security
- #hex










