diff --git a/.gitignore b/.gitignore index 49830e4..47010b0 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ deps/ ebin/ .eunit/ _build +rebar.lock diff --git a/.travis.yml b/.travis.yml index 6a6becb..da6ba92 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,4 +8,4 @@ otp_release: - R15B02 - R15B01 - R15B -before_script: "sudo apt-get --yes --force-yes install libpam0g-dev" +before_script: "sudo apt-get --yes --force-yes install libpam-runtime" diff --git a/CHANGELOG.markdown b/CHANGELOG.markdown index 5bf09a6..bc8c934 100644 --- a/CHANGELOG.markdown +++ b/CHANGELOG.markdown @@ -1,9 +1,18 @@ ## 0.5.0 (in development) +* Add `range_X` functions for getting a list of dates/times within a range + (such as `range_day/3` to get a range of days between a start and end date. +* Add `beginning_X` functions to return the beginning of the provided precision + (minute, hour, day, month, or year) * Update to rebar3 and add hex compatability. (@Licenser) * Properly add dependent apps to .app.src (@Licenser) * Remove R14 from travis testing. +## 0.4.2 + +* Add partial support for `ec_date`'s 4-tuple subsecond accuracy time format. +* Fix `erlware_commons` dependency to a rebar2-compatible version. + ## 0.4.1 * Remove unnecessary `io:format` call. diff --git a/Makefile b/Makefile index 2eb16b7..e94d5d0 100644 --- a/Makefile +++ b/Makefile @@ -5,6 +5,9 @@ all: compile compile: $(REBAR) compile +update: + $(REBAR) update + test: compile $(REBAR) eunit diff --git a/README.markdown b/README.markdown index 5fcad6f..df35ae7 100644 --- a/README.markdown +++ b/README.markdown @@ -619,25 +619,41 @@ ok %% that timezone to our intended timezone. ``` -## Date Arithmetic +## Date Truncation (Beginning of X) -(not fully tested yet, but will have full tests for 0.4.0) +Sometimes you need to truncate a time (say, the beginning of the current +month). + +This is abstracted to `beginning_X` functions, which return a date/time format +with the dates and times truncated to the specified level. + + + `beginning_minute(Date)` + + `beginning_hour(Date)` + + `beginning_day(Date)` + + `beginning_month(Date)` + + `beginning_year(Date)` + +There are also 0-arity versions of the above, in which `Date` is assumed to be +"right now". For example, calling `qdate:beginning_month()` would return +midnight on the first day of the current month. + +## Date Arithmetic The current implementation of qdate's date arithmetic returns Unixtimes. There are 8 main functions for date arithmetic: - + `add_seconds(Seconds, Date)` - + `add_minutes(Minutes, Date)` - + `add_hours(Hours, Date)` - + `add_days(Days, Date)` - + `add_weeks(Weeks, Date)` - + `add_months(Months, Date)` - + `add_years(Years, Date)` - + `add_date(DateToAdd, Date)` - `DateToAdd` is a shortcut way of adding - numerous options. For example. `qdate:add_date({{1, 2, -3}, {-500, 20, 0}})` - will add 1 year, add 2 months, subtract 3 days, subtract 500 hours, add 20 - minutes, and not make any changes to seconds. + + `add_seconds(Seconds, Date)` + + `add_minutes(Minutes, Date)` + + `add_hours(Hours, Date)` + + `add_days(Days, Date)` + + `add_weeks(Weeks, Date)` + + `add_months(Months, Date)` + + `add_years(Years, Date)` + + `add_date(DateToAdd, Date)` - `DateToAdd` is a shortcut way of adding + numerous options. For example. `qdate:add_date({{1, 2, -3}, {-500, 20, 0}})` + will add 1 year, add 2 months, subtract 3 days, subtract 500 hours, add 20 + minutes, and not make any changes to seconds. For the date arithmetic functions, `Date`, like all `qdate` functions, can be any format. @@ -654,6 +670,74 @@ There are 7 other arithmetic functions that take a single argument, and these do + `add_months(Months)` + `add_years(Years)` +## Date and Time Ranges + +qdate provides a number of `range` functions that give applicable dates/times +within a start and end time. For example, "All days from 2015-01-01 to today", +"every 3rd month from 2000-01-01 to 2009-12-31", or "every 15 minutes from +midnight to 11:59pm on 2015-04-15". + +The functions are as follows: + + + `range_seconds(Interval, Start, End)` + + `range_minutes(Interval, Start, End)` + + `range_hours(Interval, Start, End)` + + `range_days(Interval, Start, End)` + + `range_weeks(Interval, Start, End)` + + `range_months(Interval, Start, End)` + + `range_years(Interval, Start, End)` + +Where `Interval` is the number of seconds/days/years/etc. + +So for example: + +```erlang +%% Get every 15th minute from "2015-04-15 12:00am to 2015-04-15 11:59am" +> qdate:range_minutes(15, "2015-04-15 12:00am", "2015-04-15 11:59am"). +[1429056000,1429056900,1429057800,1429058700,1429059600, + 1429060500,1429061400,1429062300,1429063200,1429064100, + 1429065000,1429065900,1429066800,1429067700,1429068600, + 1429069500,1429070400,1429071300,1429072200,1429073100, + 1429074000,1429074900,1429075800,1429076700,1429077600, + 1429078500,1429079400,1429080300,1429081200|...] + +%% Get every day of April, 2014 +> qdate:range_days(1, "2014-04-01", "2014-04-30"). +[1396310400,1396396800,1396483200,1396569600,1396656000, + 1396742400,1396828800,1396915200,1397001600,1397088000, + 1397174400,1397260800,1397347200,1397433600,1397520000, + 1397606400,1397692800,1397779200,1397865600,1397952000, + 1398038400,1398124800,1398211200,1398297600,1398384000, + 1398470400,1398556800,1398643200,1398729600|...] +``` + +Note, that the return value (just like qdate's arithmetic functions) is a list +of integers. These integers are unix timestamps and can be easily formatted +with qdate: + +```erlang +> Mins = qdate:range_minutes(15, "2015-04-15 12:00am", "2015-04-15 11:59am"), +> [qdate:to_string("Y-m-d h:ia", M) || M <- Mins]. +["2015-04-15 00:00am","2015-04-15 00:15am", + "2015-04-15 00:30am","2015-04-15 00:45am", + "2015-04-15 01:00am","2015-04-15 01:15am", + "2015-04-15 01:30am","2015-04-15 01:45am", + "2015-04-15 02:00am","2015-04-15 02:15am", + "2015-04-15 02:30am","2015-04-15 02:45am", + "2015-04-15 03:00am","2015-04-15 03:15am", + "2015-04-15 03:30am","2015-04-15 03:45am", + "2015-04-15 04:00am","2015-04-15 04:15am", + "2015-04-15 04:30am","2015-04-15 04:45am", + "2015-04-15 05:00am","2015-04-15 05:15am", + "2015-04-15 05:30am","2015-04-15 05:45am", + "2015-04-15 06:00am","2015-04-15 06:15am", + "2015-04-15 06:30am","2015-04-15 06:45am", + [...]|...] +``` + +Also note that the range functions are *inclusive*. + + ## Thanks A few shoutouts to [Dale Harvey](http://github.com/daleharvey) and the @@ -683,6 +767,7 @@ See [CHANGELOG.markdown](https://github.com/choptastic/qdate/blob/master/CHANGEL + Provide a sample qdate.config for users to see + Research the viability of [ezic](https://github.com/drfloob/ezic) for a timezone backend replacement for `erlang_localtime`. ++ Add age calculation stuff: `age_years(Date)`, `age_minutes(Date)`, etc. ## Conclusion diff --git a/rebar.config b/rebar.config index 59316c2..092d6f1 100644 --- a/rebar.config +++ b/rebar.config @@ -5,7 +5,11 @@ %% For rebar2 compat {deps, [ + %% This uses an older erlware_commons version so retain compatibility with + %% rebar2. v0.16.1 introduced a 'cf' dependency, which seems to cause + %% breakage. {erlware_commons, ".*", {git, "git://github.com/erlware/erlware_commons.git", {tag, "v0.15.0"}}}, + {erlang_localtime, ".*", {git, "git://github.com/choptastic/erlang_localtime.git", {branch, master}}} ]}. diff --git a/src/qdate.app.src b/src/qdate.app.src index 4c37fdd..1c522ce 100644 --- a/src/qdate.app.src +++ b/src/qdate.app.src @@ -1,7 +1,7 @@ {application, qdate, [ {description, "Simple Date and Timezone handling for Erlang"}, - {vsn, "0.4.1"}, + {vsn, "0.4.2"}, {registered, []}, {applications, [ kernel, diff --git a/src/qdate.erl b/src/qdate.erl index 5cf7fce..7d4e959 100644 --- a/src/qdate.erl +++ b/src/qdate.erl @@ -1,5 +1,5 @@ % vim: ts=4 sw=4 et -% Copyright (c) 2013 Jesse Gumm +% Copyright (c) 2013-2015 Jesse Gumm % See LICENSE for licensing information. % -module(qdate). @@ -24,6 +24,20 @@ unixtime/0 ]). +-export([ + beginning_minute/1, + beginning_minute/0, + beginning_hour/1, + beginning_hour/0, + beginning_day/1, + beginning_day/0, + %beginning_week/2, %% needs to be /2 because we also need to define what day is considered "beginning of week", since some calendars do sunday and some do monday. We'll hold off on implementation here + beginning_month/1, + beginning_month/0, + beginning_year/1, + beginning_year/0 +]). + -export([ compare/2, compare/3 @@ -47,6 +61,17 @@ add_date/2 ]). +-export([ + range/4, + range_seconds/3, + range_minutes/3, + range_hours/3, + range_days/3, + range_weeks/3, + range_months/3, + range_years/3 +]). + -export([ register_parser/2, register_parser/1, @@ -242,7 +267,9 @@ to_date(ToTZ, Disambiguate, RawDate) -> end, try raw_to_date(RawDate3) of D={{_,_,_},{_,_,_}} -> - date_tz_to_tz(D, Disambiguate, FromTZ, ToTZ) + date_tz_to_tz(D, Disambiguate, FromTZ, ToTZ); + {{Year, Month, Date},{Hour,Minute,Second,_Millis}} -> + date_tz_to_tz({{Year, Month, Date},{Hour,Minute,Second}}, Disambiguate, FromTZ, ToTZ) catch _:_ -> case raw_to_date(RawDate) of @@ -311,6 +338,45 @@ to_now(Disamb, ToParse) -> end. +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%% Beginning/Truncation %%%%%%%%%%%%%%%%%%%%%%%%%%%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +beginning_minute() -> + beginning_minute({date(),time()}). + +beginning_minute(Date) -> + {{Y,M,D},{H,M,_}} = to_date(Date), + {{Y,M,D},{H,M,0}}. + +beginning_hour() -> + beginning_hour({date(),time()}). + +beginning_hour(Date) -> + {{Y,M,D},{H,_,_}} = to_date(Date), + {{Y,M,D},{H,0,0}}. + +beginning_day() -> + beginning_day({date(),time()}). + +beginning_day(Date) -> + {{Y,M,D},{_,_,_}} = to_date(Date), + {{Y,M,D},{0,0,0}}. + +beginning_month() -> + beginning_month({date(),time()}). + +beginning_month(Date) -> + {{Y,M,_},{_,_,_}} = to_date(Date), + {{Y,M,1},{0,0,0}}. + +beginning_year() -> + beginning_year({date(),time()}). + +beginning_year(Date) -> + {{Y,_,_},{_,_,_}} = to_date(Date), + {{Y,1,1},{0,0,0}}. + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%% Comparisons %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% @@ -458,6 +524,77 @@ fmid({Y, M, D}) when D < 1 -> fmid(Date) -> Date. +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%% Ranges %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +range(seconds, Interval, Start, Finish) -> + range_inner(fun add_seconds/2, Interval, Start, Finish); +range(minutes, Interval, Start, Finish) -> + range_inner(fun add_minutes/2, Interval, Start, Finish); +range(hours, Interval, Start, Finish) -> + range_inner(fun add_hours/2, Interval, Start, Finish); +range(days, Interval, Start, Finish) -> + range_inner(fun add_days/2, Interval, Start, Finish); +range(weeks, Interval, Start, Finish) -> + range_inner(fun add_weeks/2, Interval, Start, Finish); +range(months, Interval, Start, Finish) -> + range_inner(fun add_months/2, Interval, Start, Finish); +range(years, Interval, Start, Finish) -> + range_inner(fun add_years/2, Interval, Start, Finish). + +range_inner(IntervalFun, Interval, Start, Finish) when Interval > 0 -> + %% If Interval>0, then we're ascending, and we want to compare start/end + %% dates normally + CompareFun = fun(S, F) -> compare(S, F) end, + range_normalizer(IntervalFun, Interval, CompareFun, Start, Finish); +range_inner(IntervalFun, Interval, Start, Finish) when Interval < 0 -> + %% If Interval<0, then we're descending, and we want to compare start/end + %% dates backwards (we want to end when the Start Date is Lower than + %% Finish) + CompareFun = fun(S, F) -> compare(F, S) end, + range_normalizer(IntervalFun, Interval, CompareFun, Start, Finish); +range_inner(_, Interval, _, _) when Interval==0 -> + throw(interval_cannot_be_zero). + +range_normalizer(IntervalFun, Interval, CompareFun, Start0, Finish0) -> + %% Convert dates to unixtime for speed's sake + Start = to_unixtime(Start0), + Finish = to_unixtime(Finish0), + %% Prepare the incrementer, so we just need to pass the date to the incrementer. + Incrementer = fun(D) -> IntervalFun(Interval, D) end, + range_worker(Incrementer, CompareFun, Start, Finish). + +range_worker(Incrementer, CompareFun, Start, Finish) -> + case CompareFun(Start, Finish) of + 0 -> [Finish]; %% Equal, so we add our Finish value + 1 -> []; %% Start is after Finish, so we add nothing + -1 -> %% Start is before Finish, so we include it, and recurse + NextDay = Incrementer(Start), + [Start | range_worker(Incrementer, CompareFun, NextDay, Finish)] + end. + +range_seconds(Interval, Start, Finish) -> + range(seconds, Interval, Start, Finish). + +range_minutes(Interval, Start, Finish) -> + range(minutes, Interval, Start, Finish). + +range_hours(Interval, Start, Finish) -> + range(hours, Interval, Start, Finish). + +range_days(Interval, Start, Finish) -> + range(days, Interval, Start, Finish). + +range_weeks(Interval, Start, Finish) -> + range(weeks, Interval, Start, Finish). + +range_months(Interval, Start, Finish) -> + range(months, Interval, Start, Finish). + +range_years(Interval, Start, Finish) -> + range(years, Interval, Start, Finish). + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%% Timezone Stuff %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% @@ -780,7 +917,9 @@ tz_tests(_) -> ?_assertEqual(ok, set_timezone("EST")), ?_assertEqual(555555555,to_unixtime("1987-08-10 00:59:15 GMT")), ?_assertEqual({555,555555,0},to_now("1987-08-10 00:59:15 GMT")), - ?_assertEqual(ok, set_timezone("GMT")) + ?_assertEqual(ok, set_timezone("GMT")), + ?_assertEqual({{1970, 1, 1}, {1, 0, 0}}, to_date("CET", "1970-01-01T00:00:00Z")) + ]}.