Function To Generate All Valid Dates Between A Start and An End Date

125 views
Skip to first unread message

Onorio Catenacci

unread,
Nov 6, 2014, 4:55:35 PM11/6/14
to elixir-l...@googlegroups.com
Hi all,

I'd appreciate it if anyone would take a look at this question on Stack Exchange Code Review and share any comments anyone would care to share.


For those who don't care to go out and look at the code, here it is:

  def generate_all_valid_dates_in_range(_start_date, _end_date) when _start_date <= _end_date do
    (:calendar.date_to_gregorian_days(_start_date) .. :calendar.date_to_gregorian_days(_end_date))
    |> Enum.to_list
    |> Enum.map (&(:calendar.gregorian_days_to_date(&1)))
  end

Mainly I post the question on Code Review to help get exposure for our favorite language. 

Again, would appreciate any comments if anyone cares to share.  Mainly I'm wondering if there's already some built-in OTP function to do this (my Google Fu isn't so very good these days).

--
Onorio



Corey Haines

unread,
Nov 6, 2014, 5:11:51 PM11/6/14
to elixir-l...@googlegroups.com
Hi,

A couple thoughts, as I'm learning Elixir. Please feel free, anyone, to step in and clear up any misconceptions that I have.

On Thu, Nov 6, 2014 at 3:55 PM, Onorio Catenacci <cate...@gmail.com> wrote:
Hi all,

I'd appreciate it if anyone would take a look at this question on Stack Exchange Code Review and share any comments anyone would care to share.


For those who don't care to go out and look at the code, here it is:

  def generate_all_valid_dates_in_range(_start_date, _end_date) when _start_date <= _end_date do
    (:calendar.date_to_gregorian_days(_start_date) .. :calendar.date_to_gregorian_days(_end_date))
    |> Enum.to_list
    |> Enum.map (&(:calendar.gregorian_days_to_date(&1)))
  end


- Using a leading _ on names is used to indicate to Elixir that you won't be using those values in your code. You give it a name, so there is some documentation, instead of just a naked _

- Your map can be written as
    |> Enum.map (&:calendar.gregorian_days_to_date/1)

Just turning the function into something that map consumes, rather than building an anonymous function around it, which is what I think the &1 is going to do.

- It looks like the range operator (..) returns something that Enum.map can consume, so you don't seem to need Enum.to_list.

- Also would be nice to put a catch-all clause that raising an error for invalid inputs.

Here is my final version that appears to work. :)

defmodule DateBetween do
  def generate_all_valid_dates_in_range(start_date, end_date) when start_date <= end_date do
    (:calendar.date_to_gregorian_days(start_date) .. :calendar.date_to_gregorian_days(end_date))
    |> Enum.map (&:calendar.gregorian_days_to_date/1)
  end
  def generate_all_valid_dates_in_range(_start_date, _end_date), do: raise "start_date must not be after end_date"
end

-Corey







 
Mainly I post the question on Code Review to help get exposure for our favorite language. 

Again, would appreciate any comments if anyone cares to share.  Mainly I'm wondering if there's already some built-in OTP function to do this (my Google Fu isn't so very good these days).

--
Onorio



--
You received this message because you are subscribed to the Google Groups "elixir-lang-talk" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-ta...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Alex Shneyderman

unread,
Nov 6, 2014, 5:38:37 PM11/6/14
to elixir-l...@googlegroups.com
use timex it is awesome.

defmodule TimexTest do
    use ExUnit.Case
    use Timex
    
    @moduletag :all
    @moduletag :unit

    def gen(acc, _, _, cr) when cr == 0 or cr == -1, do: acc
    def gen(acc, cdt, edt, 1) do
        ndt = Date.shift(cdt, days: 1)
        gen([cdt | acc], ndt, edt, Date.compare(edt, ndt))
    end    

    test "s" do
        {:ok, s} = TimexParsers.Parser.parse("2014-07-21T00:20:41.196Z", "{ISOz}")
        {:ok, f} = TimexParsers.Parser.parse("2014-07-29T00:20:41.196Z", "{ISOz}")

        dates = gen([], s, f, 1)
        9 = length(dates)
        IO.puts "\n\n#{inspect(dates)}"

    end   
end

all the erlang date/time functionality makes me want to vomit.

Alex Shneyderman

unread,
Nov 6, 2014, 5:47:23 PM11/6/14
to elixir-l...@googlegroups.com
oops, I obviously meant this -

defmodule TimexTest do
    use ExUnit.Case
    use Timex
    
    @moduletag :all
    @moduletag :unit

    def gen(acc, _, _, -1), do: acc
    def gen(acc, cdt, edt, _) do

Onorio Catenacci

unread,
Nov 7, 2014, 8:11:04 AM11/7/14
to elixir-l...@googlegroups.com
On Thursday, November 6, 2014 5:11:51 PM UTC-5, Corey Haines wrote:
Hi,

A couple thoughts, as I'm learning Elixir. Please feel free, anyone, to step in and clear up any misconceptions that I have.


Thanks Corey.  Responses inlined. 

 
On Thu, Nov 6, 2014 at 3:55 PM, Onorio Catenacci <cate...@gmail.com> wrote:
Hi all,

I'd appreciate it if anyone would take a look at this question on Stack Exchange Code Review and share any comments anyone would care to share.


For those who don't care to go out and look at the code, here it is:

  def generate_all_valid_dates_in_range(_start_date, _end_date) when _start_date <= _end_date do
    (:calendar.date_to_gregorian_days(_start_date) .. :calendar.date_to_gregorian_days(_end_date))
    |> Enum.to_list
    |> Enum.map (&(:calendar.gregorian_days_to_date(&1)))
  end


- Using a leading _ on names is used to indicate to Elixir that you won't be using those values in your code. You give it a name, so there is some documentation, instead of just a naked _

I have a habit (albeit maybe not the best habit) of starting with all my parameters named with leading underscores so I don't get the unused variable warnings as I'm working through the code.  While the snippet I posted above may seem simple and obvious, it was actually the product of four or five failed attempts to figure out how to generate a range of valid dates between a given starting date and ending date.  Of course before I move anything into production, I remove the leading underscores because I want the compiler warning if I've forgotten and left in an unneeded variable.

 

- Your map can be written as
    |> Enum.map (&:calendar.gregorian_days_to_date/1)

Just turning the function into something that map consumes, rather than building an anonymous function around it, which is what I think the &1 is going to do.

You're right and it is a bit clearer the way you coded it. 


- It looks like the range operator (..) returns something that Enum.map can consume, so you don't seem to need Enum.to_list.

Good point.  
 

- Also would be nice to put a catch-all clause that raising an error for invalid inputs.

Also a good point but maybe not the way you want Elixir code to work.  Generally want things to fail fast; if someone calls the function with invalid dates you probably want things to fail right there rather than raising and catching an exception.  Or maybe I'm misunderstanding something myself.
 
At any rate, thanks for your suggestions and comments.  That's exactly the sort of thing I was hoping for.

Onorio Catenacci

unread,
Nov 7, 2014, 8:12:48 AM11/7/14
to elixir-l...@googlegroups.com
On Thursday, November 6, 2014 5:38:37 PM UTC-5, Alex Shneyderman wrote:
use timex it is awesome.

defmodule TimexTest do
    use ExUnit.Case
    use Timex
    
    @moduletag :all
    @moduletag :unit

    def gen(acc, _, _, cr) when cr == 0 or cr == -1, do: acc
    def gen(acc, cdt, edt, 1) do
        ndt = Date.shift(cdt, days: 1)
        gen([cdt | acc], ndt, edt, Date.compare(edt, ndt))
    end    

    test "s" do
        {:ok, s} = TimexParsers.Parser.parse("2014-07-21T00:20:41.196Z", "{ISOz}")
        {:ok, f} = TimexParsers.Parser.parse("2014-07-29T00:20:41.196Z", "{ISOz}")

        dates = gen([], s, f, 1)
        9 = length(dates)
        IO.puts "\n\n#{inspect(dates)}"

    end   
end

all the erlang date/time functionality makes me want to vomit.

That's good to know about Alex.  Thanks for pointing it out.

--
Onorio
 
 

Saša Jurić

unread,
Nov 7, 2014, 8:56:39 AM11/7/14
to elixir-l...@googlegroups.com

I have a habit (albeit maybe not the best habit) of starting with all my parameters named with leading underscores so I don't get the unused variable warnings as I'm working through the code.  While the snippet I posted above may seem simple and obvious, it was actually the product of four or five failed attempts to figure out how to generate a range of valid dates between a given starting date and ending date.  Of course before I move anything into production, I remove the leading underscores because I want the compiler warning if I've forgotten and left in an unneeded variable.

I believe this is not the best habit. It's error prone, because you need to remove leading underscores from all variables which you use. To do this, you need to manually inspect your changes, and be careful not to miss any variable which is used. This is error prone, and becomes very hard to get right once your project becomes more complex, and you make various changes. Ultimately, this approach increases the probability that you'll have an anonymous variable which is in fact used.

I would advise using proper named variables immediately, ignoring the warnings during the dev cycle, and then once you're happy with the behavior, cleaning up the code by making unused variable anonymous. This approach is less error prone, because once you're done with your changes, you just have to flush out remaining compiler warnings, so it's easy to see what needs to be done.

Onorio Catenacci

unread,
Nov 7, 2014, 8:58:58 AM11/7/14
to elixir-l...@googlegroups.com
Excellent points Saša.  Very good reasons to stop my habit.

--
Onorio

José Valim

unread,
Nov 7, 2014, 8:59:40 AM11/7/14
to elixir-l...@googlegroups.com
I would advise using proper named variables immediately, ignoring the warnings during the dev cycle, and then once you're happy with the behavior, cleaning up the code by making unused variable anonymous.

In fact, with time you may even start using the unused variable warning to detect early bugs in your code: "wait, that varible is not being used? something is wrong..."

Sasa Juric

unread,
Nov 7, 2014, 9:01:27 AM11/7/14
to elixir-l...@googlegroups.com
On 07 Nov 2014, at 14:59, José Valim <jose....@plataformatec.com.br> wrote:

I would advise using proper named variables immediately, ignoring the warnings during the dev cycle, and then once you're happy with the behavior, cleaning up the code by making unused variable anonymous.

In fact, with time you may even start using the unused variable warning to detect early bugs in your code: "wait, that varible is not being used? something is wrong…"

Indeed, this happens to me frequently :-)

José Valim

unread,
Nov 7, 2014, 9:02:48 AM11/7/14
to elixir-l...@googlegroups.com
 
 
Also a good point but maybe not the way you want Elixir code to work.  Generally want things to fail fast; if someone calls the function with invalid dates you probably want things to fail right there rather than raising and catching an exception.

This is typically how Elixir/Erlang code is written indeed. If a condition is not met, it fails as MatchError or FunctionClauseError. We typically don't add an extra clause saying what went wrong. The upside is that we simply worry about the happy path (forcing us to write more assertive code). The downside is that it may be cryptic sometimes to find exactly what went wrong. We do include the arguments in the stacktrace though.

Corey Haines

unread,
Nov 7, 2014, 9:08:25 AM11/7/14
to elixir-l...@googlegroups.com
This is a great point. Thanks for reminding me!

-Corey

Lau Taarnskov

unread,
Jan 14, 2015, 11:33:40 AM1/14/15
to elixir-l...@googlegroups.com

I'd appreciate it if anyone would take a look at this question on Stack Exchange Code Review and share any comments anyone would care to share. 

With the Kalends library ( https://github.com/lau/kalends ) you can now get a stream of the dates between two specified dates.

{:ok, from} = Kalends.Date.from_erl {2015, 1, 14}
{:ok, to}     = Kalends.Date.from_erl {2015, 1, 20}
Kalends.Date.stream(from, to) |> Enum.to_list

The first two lines validates and assigns the dates to the variables "from" and "to". In this case the 14th and 20th of January 2015.
The third line gets a stream of Date structs and then pipes them to Enum.to_list so that we get a list with both dates and all valid dates in between:

[%Kalends.Date{day: 14, month: 1, year: 2015},
 %Kalends.Date{day: 15, month: 1, year: 2015},
 %Kalends.Date{day: 16, month: 1, year: 2015},
 %Kalends.Date{day: 17, month: 1, year: 2015},
 %Kalends.Date{day: 18, month: 1, year: 2015},
 %Kalends.Date{day: 19, month: 1, year: 2015},
 %Kalends.Date{day: 20, month: 1, year: 2015}]

If you want erlang style date tuples instead you can pipe that result into a function that does that: |> Enum.map &(Kalends.Date.to_erl &1)
[{2015, 1, 14}, {2015, 1, 15}, {2015, 1, 16}, {2015, 1, 17}, {2015, 1, 18}, {2015, 1, 19}, {2015, 1, 20}]

--
Lau Taarnskov
twitter: @laut ( https://twitter.com/laut )
Reply all
Reply to author
Forward
0 new messages