Working directly on DateObjects

This relates to an earlier question I asked some years ago, From a list of dates, get a list of the last date available in each month.

In this case, I have a list of business dates (no weekends or holidays) as DateObjects in the list dates. I want to extract the 1st available DateObject for each month in the list.

The following code works fine.

Min[DateObject /@ #] & /@ SplitBy[dates, DateList[#][[2]] &];


Does a way exist to do the above directly on each DateObject rather than converting each to a DateList as in DateList[#][[2]]?

• If you already have a DateObject you can get the month using DateValue[d, "Month"] Commented Mar 6, 2023 at 20:35
• GroupBy[dates, #["Month"] &, Min]?
– kglr
Commented Mar 6, 2023 at 20:51
• @JasonB. - I don't need the month. I need the the first DateObject for each month. Commented Mar 6, 2023 at 21:03

The way that you are currently working is better than the proposed alternatives.

Clear["Global*"]

SeedRandom[1234];

dates = RandomDate[{DateObject[{2022, 1}, "Month"],
DateObject[{2022, 12}, "Month"]}, 100];


Current method (note that dates must be sorted)

t1 = RepeatedTiming[
min1 = Min[DateObject /@ #] & /@
SplitBy[Sort@dates, DateList[#][[2]] &];][[1]]

(* 0.00127546 *)


Using DateValue (dates must be sorted)

t2 = RepeatedTiming[
min2 = Min[DateObject /@ #] & /@
SplitBy[Sort@dates, DateValue[#, "Month"] &];][[1]]

(* 0.00940084 *)


Using GroupBy (dates do not need to be sorted)

t3 = RepeatedTiming[min3 = GroupBy[dates, #["Month"] &, Min];][[1]]

0.0054788


Using GatherBy (dates do not need to be sorted)

t4 = RepeatedTiming[min4 = Min /@ GatherBy[dates, #["Month"] &];][[1]]

0.00546494


All methods give equivalent results

min1 === min2 === Sort@Normal[min3][[All, -1]] === Sort@min4

(* True *)


Your current method is most efficient of the given methods

{t1, t2, t3, t4}/t1 *)

(* {1., 7.37056, 4.29556, 4.28468}

• using DateList[#][[2]]& instead of #["Month"]& in the second argument gives better performance for GroupBy and GatherBy. (+1)
– kglr
Commented Mar 6, 2023 at 21:59
• you can also use Min instead of Min[DateObject /@ #] & in methods 1 and 2.
– kglr
Commented Mar 6, 2023 at 22:04

As pointed out in this comment the code

GroupBy[dates, #["Month"] &, Min]


is equivalent to your code and avoids the use of DateList to deconstruct the date objects. The above code is much more readable, and as we see in this answer the timing difference is negligible.

strings = {"Thu 30 Mar 2023 08:52:48",
"Tue 14 Feb 2023 18:02:16", "Sun 5 Feb 2023 14:44:49",
"Sat 7 Jan 2023 14:29:20", "Mon 9 Jan 2023 21:16:06",
"Fri 13 Jan 2023 01:11:07", "Sun 4 Sep 2022 10:33:07",
"Thu 8 Sep 2022 20:03:03", "Sat 23 Jul 2022 22:24:56",
"Fri 29 Jul 2022 01:04:59", "Fri 5 Aug 2022 17:29:07",
"Sat 24 Sep 2022 08:18:38"};

dates = DateObject /@ strings

(*
#1[[2,-1]] & is the function that selects the the last date in month.
#1[[2,1]] would delect the desired first date in mpnth
*)

AssociationMap[#1[[1]] -> Association[Table[m[[1,2]] -> m[[2]],
{m, #1[[2]]}]] & ,
GroupBy[Normal[AssociationMap[#1[[1]] -> #1[[2,-1]] & ,
GroupBy[Sort[dates], #1[[1,1 ;; 2]] & ]]], #1[[1,1]] & ]]

Association[2022 -> Association[
7 -> DateObject[{2022, 7, 29, 1, 4, 59.}, "Instant",
"Gregorian", -5.], 8 -> DateObject[{2022, 8, 5, 17, 29,
7.}, "Instant", "Gregorian", -5.],
9 -> DateObject[{2022, 9, 24, 8, 18, 38.}, "Instant",
"Gregorian", -5.]],
2023 -> Association[1 -> DateObject[{2023, 1, 13, 1, 11, 7.},
"Instant", "Gregorian", -5.],
2 -> DateObject[{2023, 2, 14, 18, 2, 16.}, "Instant",
"Gregorian", -5.], 3 -> DateObject[{2023, 3, 30, 8, 52,
48.}, "Instant", "Gregorian", -5.]]]


Of course, I may have missed something. An Association is much like a hash map.

Assuming:

dates = RandomDate[DateObject /@ {{2020, 1, 1}, {2022, 1, 1}}, 100];

(* Baseline *)
r0 = Min[DateObject /@ #] & /@
SplitBy[dates, DateList[#][[2]] &]; // RepeatedTiming

(* Out: 0.00248583 *)

Iteration 1

We have a dedicated function to get the minimum from a list of dates called MinDate (it's slower but it's a built-in :)

r1 = MinDate /@ SplitBy[dates, DateList[#][[2]] &]; // RepeatedTiming

(* Out: 0.00680119 - 0.36 baseline *)

Iteration 2

We can replace MinDate with Min:

r2 = Min /@ SplitBy[dates, DateList[#][[2]] &]; // RepeatedTiming

(* Out: 0.00216887 - 1.14 baseline *)

Iteration 3

For some reason First@*Sort is faster than Min:

r3 = First @* Sort /@
SplitBy[dates, DateList[#][[2]] &]; // RepeatedTiming

(* Out: 0.00186683 - 1.33 baseline *)

Iteration 4

Since you'll probably use the gregorian calendar, I think it's safe to get the month out of DateObject directly:

r4 = First @* Sort /@ SplitBy[dates, #[[1, 2]] &]; // RepeatedTiming

(* Out: 0.000433229 - 5.7 baseline *)

Iteration 5

We can squeeze more performance by extracting in batches:

r5 = (Sort /@ SplitBy[dates, #[[1, 2]] &])[[All, 1]]; // RepeatedTiming

(* Out: 0.000405205 - 6.1 baseline *)

Iteration 6

We can go all out to squeeze as much as we can using an undocumented function:

Block[{temp = Split @ dates[[All, 1, 2]]},
r6 = (Sort /@ InternalCopyListStructure[temp, dates])[[All, 1]];
]; // RepeatedTiming

(* Out: 0.0000837435 - 29.6 baseline *)


And of course, the outputs are the same:

r0 === r1 (* True *)
r0 === r2 (* True *)
r0 === r3 (* True *)
r0 === r4 (* True *)
r0 === r5 (* True *)
r0 === r6 (* True *)