# Using PatternTest and/or Condition to detect equal list lengths as well as list element formats

On searching Mathematica.SE for information about overloading functions, I came across several threads about PatternTest and Condition, and I understand the basic examples like matching by EvenQ. However I'm having trouble imagining how I'd use these features to implement checks having to do with "whole" lists.

For example, I'd like to define a function Convert of the form

Convert[quantities_, prices_] :=


only if quantities and prices are equal-length lists. In other words, if Length[quantities] != Length[prices] then Convert should behave as if undefined.

For a more complex example, I'd like to define an overloaded version,

Convert[dateQuantityList_, datePriceList_] :=


where dateQuantityList and datePriceList look like

{{{2012, 1, 1, 9, 30, 7}, 150},
{{2012, 1, 1, 9, 30, 12}, 130},
{{2012, 1, 1, 9, 30, 33}, 470},
...
}


and

{{{2012, 1, 1, 9, 30, 7}, 24.56},
{{2012, 1, 1, 9, 30, 33}, 24.58},
...
}


such that the function is only defined if all elements in dateQuantityList[[All, 1]] and datePriceList[[All, 1]] are valid DateLists. (Is there something like a DateListQ? But even supposing so, again, I'm not clear on how to apply PatternTest or Condition to a property of an entire list, not its elements...)

(The goal of the latter overload is to do a "left join" based on the date, i.e. discard prices that don't have corresponding dates in the quantities list, and calling the original form, Convert[q_, p_]. I know how to do that part using GatherBy—it's the PatternTest/Condition part that's completely new to me.)

I have many more overloads I'd like to write, but if I can get started with those two examples, I think I can figure the rest out.

There is a build-in function DateQ. It is a very nice function. For example it knows that 2000 is a leap year, but 2100 is not

Needs["Calendar"]
DateQ[{2000, 2, 29}]
DateQ[{2100, 2, 29}]


True

False

DateQ can take hours, minutes and seconds, but it ignores their values. See definition (there is no CalendarPrivater in the rhs):

DateQ[{CalendarPrivatey_, CalendarPrivatem_, CalendarPrivated_,
CalendarPrivater___}, CalendarPrivateopts___] :=
Module[{CalendarPrivatecal =
Calendar /. Flatten[{CalendarPrivateopts, Options[DateQ]}]},
CalendarPrivatedateQ[CalendarPrivatey, CalendarPrivatem,
CalendarPrivated, CalendarPrivatecal]]


However you can implement your own dateTimeQ

dateTimeQ[{h_, m_, d_, hour_, min_, sec_}] :=
MatchQ[{h, m, d, hour, min, sec}, {__Integer}] && DateQ[{h, m, d}] &&
0 <= hour < 24 && 0 <= min < 60 && 0 <= sec < 60;
dateTimeQ[___] := False


and convert

convert[quants : {{_?dateTimeQ, _Integer} ..}, prices : {{_?dateTimeQ, _Real} ..}] /;
Length@quants == Length@prices := OK;

quant = {{{2012, 1, 1, 9, 30, 7}, 150},
{{2012, 1, 1, 9, 30, 12}, 130},
{{2012, 1, 1, 9, 30, 33}, 470}};
price = {{{2012, 1, 1, 9, 30, 7}, 1.50},
{{2012, 1, 1, 9, 30, 33}, 1.30},
{{2012, 1, 1, 9, 30, 33}, 4.70}};
convert[quant, price]


OK

Convert[quantities_, prices_] /; Length[quantities] == Length[prices] := ...


Since DateList returns a list of Length 6 and the first five elements have Head Integer while the last has Head Real you can write your own DateListQ. Here is a head start:

 DateListQ[lis_List] := Length[lis] == 6 && VectorQ[Most[lis], IntegerQ] &&


But note that this is an example and if you're inputting your DateList by hand and the last element is also an Integer then you can simply use:

DateListQ[lis_List] := Length[lis] == 6 && VectorQ[lis, IntegerQ]


Now for your second case, you can do something like:

Convert[dateQuantityList_, datePriceList_] /; Length[dateQuantityList] == Length[datePriceList] &&
VectorQ[Map[DateListQ, dateQuantityList[[All, 1]], {1}], TrueQ] &&
VectorQ[Map[DateListQ, datePriceList[[All, 1]], {1}], TrueQ] := ...


OR more elegantly:

Convert[dateQuantityList_, datePriceList_] /; Length[dateQuantityList] == Length[datePriceList] &&
VectorQ[dateQuantityList[[All,1]], DateListQ] && VectorQ[datePriceList[[All,1]], DateListQ] := ...

• Putting the /; outside the parameter list... brilliant. Okay, I'm gonna give this a shot and edit my question when I figure it out! Thanks! – Andrew Cheong Oct 19 '13 at 4:54

There are endless ways to restrict the kind of arguments your convert function will accept. I will propose one in this answer which narrows acceptance quite a bit but not perfectly.

First, I define a set of predicates that provide quasi-validation of date-time lists. I say quasi-validation because I do not deal with the variation in the length of months and because my validation of years may not fit your needs. The year validation is easy to modify to accept what you consider the appropriate range.

yearQ[_Integer?(1970 <= # <= 2030 &)] := True
yearQ[___] := False

monthQ[_Integer?(1 <= # <= 12 &)] := True
monthQ[___] := False

dayQ[_Integer?(1 <= # <= 31 &)] := True
dayQ[___] := False

hourQ[_Integer?(0 <= # <= 23 &)] := True
hourQ[___] := False

secondQ = minuteQ;

dateQ[{_?yearQ, _?monthQ, _?dayQ, _?hourQ, _?minuteQ, _?secondQ}] := True
dateQ[___] := False


Given these predicates, I define two argument patterns, one for dated quantity records and the other for dated price records.

dateQuantity = {_?dateQ, _Integer};
datePrice = {_?dateQ, _Real};


Now everything is in place to write the argument pattern for convert

convert[quants : {dateQuantity ..}, prices : {datePrice ..}] /;
Length@quants == Length@prices := OK;


I think the above meets the requirements stated in your question. It certainly accepts sample data of the sort you posted, but it should get much more testing to be sure it is sufficiently restrictive.

quant = {
{{2012, 1, 1, 9, 30, 7}, 150},
{{2012, 1, 1, 9, 30, 12}, 130},
{{2012, 1, 1, 9, 30, 33}, 470}};
price = {
{{2012, 1, 1, 9, 30, 7}, 1.50},
{{2012, 1, 1, 9, 30, 33}, 1.30},
{{2012, 1, 1, 9, 30, 33}, 4.70}};
convert[quant, price]

OK


But if the arguments given in the wrong order (just to examine one failure mode)_

convert[price, quant]

convert[{{{2012, 1, 1, 9, 30, 7}, 1.5},
{{2012, 1, 1, 9, 30, 33}, 1.3},
{{2012, 1, 1, 9, 30, 33}, 4.7}},
{{{2012, 1, 1, 9, 30, 7}, 150},
{{2012, 1, 1, 9, 30, 12}, 130},
{{2012, 1, 1, 9, 30, 33}, 470}}]
`