BenFenner

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

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

BenFenner

I went with the pattern matching capability of functions as suggested, and ended up with this which works great. :smiley:

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

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. :slight_smile:

amarraja

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 :slight_smile:

@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

Where Next?

Popular in Questions Top

electic
Hi, I am new to Elixir. I am trying to use the DateTime component to insert a date into MySQL however the there seems to be no way to fo...
New
sergio
In Ruby, I can go: User.find_by(email: "foobar@email.com").update(email: "hello@email.com") How can I do something similar in Elixir? ...
New
baxterw3b
Hi guys, i’m new in the Elixir world, and i have to say, that i love it! i’m having some problem to understand anonymous functions with ...
New
JulienCorb
I am trying to implement my new.html.eex file to create new posts on my website. new.html.eex: &lt;h1&gt;Create Post&lt;/h1&gt; &lt;%= ...
New
itssasanka
Hi all, Trying to get some more clarity over utc_datetime and naive_datetime for Ecto: The documentation above suggests that while ...
New
SoCreat
i’m a new one to elixir which editor can i use vs code? or atom? Thanks! :smiley:
New
sergio_101
I am VERY much an elixir newbie. I have taken one elixir course and one phoenix course on Udemy. During that course, I saw the instructor...
New
jason.o
In the code below, if the create action is not set to accept “extra_key” as an input, it errors out with a message shown above. Is there ...
New
dotdotdotPaul
Okay, I’m having a heck of a time trying to figure out how to best handle the validation of belongs_to associations in Ecto. I’m sure I’...
New
svb
Hi! Currently I want to submit a form by pressing the Enter key. However, since my input field is of type “textarea” this is just adds a...
New

Other popular topics Top

marius95
Hello everyone, I try to use an Javascript Event Handler in my root.html.leex file. Therefore I created a function in the app.js file: ...
New
danschultzer
None of the current solutions worked well for me, so I went ahead and built a user management system from scratch. This project took far...
548 29703 241
New
shahryarjb
Hello, I have map which I want to convert it to string like this: the map: %{last_name: "tavakkoli", name: "shahryar"} the string I ne...
New
chrismccord
Phoenix 1.4.0 released Phoenix 1.4 is out! This release ships with exciting new features, most notably with HTTP2 support, improved deve...
688 31194 112
New
ovidiubadita
Hey all, I discovered Elixir and I love it. I always wanted to learn a functional programming and I intended to go for Haskell, but afte...
New
gausby
I asked this very same question on twitter and got some interesting feedback, but I thought it would be a good question to ask here as we...
1207 39523 209
New
ashish173
I am using Ecto timestamps with postgres, I can see the timestamps() use the :naive_dateime but for my use case I wanted to store the ti...
New
openscript
Hello! Sorry for this astonishing simple question, but I’m really stuck. I try to set up the intellij-elixir plugin, but I don’t know ho...
New
dokuzbir
I want to highlight html closing tags when i click a html tag. That works in .html files but doesnt work for html.eex templates. How can...
New
lanycrost
Hi everyone! I need implement if…else if…else condition from my elixir code, and anymore of this control flow structures not work proper...
New

We're in Beta

About us Mission Statement