Browse Source

Added "now" tag and associated associated dateformat module.


git-svn-id: http://erlydtl.googlecode.com/svn/trunk@131 a5195066-8e3e-0410-a82a-05b01b1b9875
colm.dougan 17 years ago
parent
commit
7bf8975725

+ 14 - 4
Makefile

@@ -1,14 +1,24 @@
 ERL=erl
 ERL=erl
+ERLC=erlc
 
 
-all:
+PARSER=src/erlydtl/erlydtl_parser
+
+all: $(PARSER).erl
 	$(ERL) -make 
 	$(ERL) -make 
 
 
+$(PARSER).erl: $(PARSER).yrl
+	$(ERLC) -o src/erlydtl src/erlydtl/erlydtl_parser.yrl
+ 
 run:
 run:
-	$(ERL) -pa `pwd`/ebin
+	$(ERL) -pa ebin
+
 
 
 test:
 test:
-	$(ERL) -noshell -s erlydtl_unittests run_tests
+	$(ERL) -noshell -pa ebin \
+		-s erlydtl_unittests run_tests \
+		-s erlydtl_dateformat_tests run_tests \
+		-s init stop
 	
 	
 clean:
 clean:
 	rm -fv ebin/*.beam
 	rm -fv ebin/*.beam
-	rm -fv erl_crash.dump
+	rm -fv erl_crash.dump $(PARSER).erl

+ 357 - 0
src/erlydtl/dateformat.erl

@@ -0,0 +1,357 @@
+-module(dateformat).
+-export([format/1, format/2]).
+
+-define(TAG_SUPPORTED(C),
+    C =:= $a orelse
+    C =:= $A orelse
+    C =:= $b orelse
+    C =:= $B orelse
+    C =:= $d orelse
+    C =:= $D orelse
+    C =:= $f orelse
+    C =:= $F orelse
+    C =:= $g orelse
+    C =:= $G orelse
+    C =:= $h orelse
+    C =:= $H orelse
+    C =:= $i orelse
+    C =:= $I orelse
+    C =:= $j orelse
+    C =:= $l orelse
+    C =:= $L orelse
+    C =:= $m orelse
+    C =:= $M orelse
+    C =:= $n orelse
+    C =:= $N orelse
+    C =:= $O orelse
+    C =:= $P orelse
+    C =:= $r orelse
+    C =:= $s orelse
+    C =:= $S orelse
+    C =:= $t orelse
+    C =:= $T orelse
+    C =:= $U orelse
+    C =:= $w orelse
+    C =:= $W orelse
+    C =:= $y orelse
+    C =:= $Y orelse
+    C =:= $z orelse
+    C =:= $Z
+).
+
+%
+% Format the current date/time
+%
+format(FormatString) ->
+    {Date, Time} = erlang:localtime(),
+    replace_tags(Date, Time, FormatString).
+%
+% Format a tuple of the form {{Y,M,D},{H,M,S}}
+% This is the format returned by erlang:localtime()
+% and other standard date/time BIFs
+%
+format({{_,_,_} = Date,{_,_,_} = Time}, FormatString) ->
+   replace_tags(Date, Time, FormatString);
+%
+% Format a tuple of the form {Y,M,D}
+%
+format({_,_,_} = Date, FormatString) ->
+   replace_tags(Date, {0,0,0}, FormatString);
+format(DateTime, FormatString) ->
+   io:format("Unrecognised date paramater : ~p~n", [DateTime]),
+   FormatString.
+
+replace_tags(Date, Time, Input) ->
+    replace_tags(Date, Time, Input, [], noslash).
+replace_tags(_Date, _Time, [], Out, _State) ->
+    lists:reverse(Out);
+replace_tags(Date, Time, [C|Rest], Out, noslash) when ?TAG_SUPPORTED(C) ->
+    replace_tags(Date, Time, Rest,
+       lists:reverse(tag_to_value(C, Date, Time)) ++ Out, noslash);
+replace_tags(Date, Time, [$\\|Rest], Out, noslash) ->
+    replace_tags(Date, Time, Rest, Out, slash);
+replace_tags(Date, Time, [C|Rest], Out, slash) ->
+    replace_tags(Date, Time, Rest, [C|Out], noslash);
+replace_tags(Date, Time, [C|Rest], Out, _State) ->
+    replace_tags(Date, Time, Rest, [C|Out], noslash).
+
+
+%-----------------------------------------------------------
+% Time formatting
+%-----------------------------------------------------------
+
+% 'a.m.' or 'p.m.'
+tag_to_value($a, _, {H, _, _}) when H > 11 -> "p.m.";
+tag_to_value($a, _, _) -> "a.m.";
+
+% 'AM' or 'PM'
+tag_to_value($A, _, {H, _, _}) when H > 11 -> "PM";
+tag_to_value($A, _, _) -> "AM";
+
+% Swatch Internet time
+tag_to_value($B, _, _) ->
+   ""; % NotImplementedError
+
+%
+% Time, in 12-hour hours and minutes, with minutes
+% left off if they're zero.
+%
+% Examples: '1', '1:30', '2:05', '2'
+%
+% Proprietary extension.
+%
+tag_to_value($f, Date, {H, 0, S}) ->
+   % If min is zero then return the hour only
+   tag_to_value($g, Date, {H, 0, S});
+tag_to_value($f, Date, Time) ->
+   % Otherwise return hours and mins
+   tag_to_value($g, Date, Time)
+      ++ ":" ++ tag_to_value($i, Date, Time);
+
+% Hour, 12-hour format without leading zeros; i.e. '1' to '12'
+tag_to_value($g, _, {H,_,_}) ->
+   integer_to_list(hour_24to12(H));
+
+% Hour, 24-hour format without leading zeros; i.e. '0' to '23'
+tag_to_value($G, _, {H,_,_}) ->
+   integer_to_list(H);
+
+% Hour, 12-hour format; i.e. '01' to '12'
+tag_to_value($h, _, {H,_,_}) ->
+   integer_to_list_zerofill(integer_to_list(hour_24to12(H)));
+
+% Hour, 24-hour format; i.e. '00' to '23'
+tag_to_value($H, _, {H,_,_}) ->
+   integer_to_list_zerofill(H);
+
+% Minutes; i.e. '00' to '59'
+tag_to_value($i, _, {_,M,_}) ->
+   integer_to_list_zerofill(M);
+
+% Time, in 12-hour hours, minutes and 'a.m.'/'p.m.', with minutes left off
+% if they're zero and the strings 'midnight' and 'noon' if appropriate.
+% Examples: '1 a.m.', '1:30 p.m.', 'midnight', 'noon', '12:30 p.m.'
+% Proprietary extension.
+tag_to_value($P, _, {0,  0, _}) -> "midnight";
+tag_to_value($P, _, {12, 0, _}) -> "noon";
+tag_to_value($P, Date, Time) ->
+   tag_to_value($f, Date, Time)
+      ++ " " ++ tag_to_value($a, Date, Time);
+
+% Seconds; i.e. '00' to '59'
+tag_to_value($s, _, {_,_,S}) ->
+   integer_to_list_zerofill(S);
+
+%-----------------------------------------------------------
+% Date formatting
+%-----------------------------------------------------------
+
+% Month, textual, 3 letters, lowercase; e.g. 'jan'
+tag_to_value($b, {_,M,_}, _) ->
+   string:sub_string(monthname(M), 1, 3);
+
+% Day of the month, 2 digits with leading zeros; i.e. '01' to '31'
+tag_to_value($d, {_, _, D}, _) ->
+   integer_to_list_zerofill(D);
+
+% Day of the week, textual, 3 letters; e.g. 'Fri'
+tag_to_value($D, Date, _) ->
+   Dow = calendar:day_of_the_week(Date),
+   ucfirst(string:sub_string(dayname(Dow), 1, 3));
+
+% Month, textual, long; e.g. 'January'
+tag_to_value($F, {_,M,_}, _) ->
+   ucfirst(monthname(M));
+
+% '1' if Daylight Savings Time, '0' otherwise.
+tag_to_value($I, _, _) ->
+   "TODO";
+
+% Day of the month without leading zeros; i.e. '1' to '31'
+tag_to_value($j, {_, _, D}, _) ->
+   integer_to_list(D);
+
+% Day of the week, textual, long; e.g. 'Friday'
+tag_to_value($l, Date, _) ->
+   ucfirst(dayname(calendar:day_of_the_week(Date)));
+
+% Boolean for whether it is a leap year; i.e. True or False
+tag_to_value($L, {Y,_,_}, _) ->
+   case calendar:is_leap_year(Y) of
+   true -> "True";
+   _ -> "False"
+   end;
+
+% Month; i.e. '01' to '12'
+tag_to_value($m, {_, M, _}, _) ->
+   integer_to_list_zerofill(M);
+
+% Month, textual, 3 letters; e.g. 'Jan'
+tag_to_value($M, {_,M,_}, _) ->
+   ucfirst(string:sub_string(monthname(M), 1, 3));
+
+% Month without leading zeros; i.e. '1' to '12'
+tag_to_value($n, {_, M, _}, _) ->
+   integer_to_list(M);
+
+% Month abbreviation in Associated Press style. Proprietary extension.
+tag_to_value($N, {_,M,_}, _) when M =:= 9 ->
+   % Special case - "Sept."
+   ucfirst(string:sub_string(monthname(M), 1, 4)) ++ ".";
+tag_to_value($N, {_,M,_}, _) when M < 3 orelse M > 7 ->
+   % Jan, Feb, Aug, Oct, Nov, Dec are all
+   % abbreviated with a full-stop appended.
+   ucfirst(string:sub_string(monthname(M), 1, 3)) ++ ".";
+tag_to_value($N, {_,M,_}, _) ->
+   % The rest are the fullname.
+   ucfirst(monthname(M));
+
+% Difference to Greenwich time in hours; e.g. '+0200'
+tag_to_value($O, Date, Time) ->
+   Diff = utc_diff(Date, Time),
+   Offset = case utc_diff(Date, Time) of
+      Diff when abs(Diff) > 2400 -> "+0000";
+      Diff when Diff < 0 ->
+          io_lib:format("-~4..0w", [trunc(abs(Diff))]);
+      _ ->
+          io_lib:format("+~4..0w", [trunc(abs(Diff))])
+   end,
+   lists:flatten(Offset);
+
+% RFC 2822 formatted date; e.g. 'Thu, 21 Dec 2000 16:01:07 +0200'
+tag_to_value($r, Date, Time) ->
+   replace_tags(Date, Time, "D, j M Y H:i:s O");
+
+% English ordinal suffix for the day of the month, 2 characters;
+% i.e. 'st', 'nd', 'rd' or 'th'
+tag_to_value($S, {_, _, D}, _) when
+   D rem 100 =:= 11 orelse
+   D rem 100 =:= 12 orelse
+   D rem 100 =:= 13 -> "th";
+tag_to_value($S, {_, _, D}, _) when D rem 10 =:= 1 -> "st";
+tag_to_value($S, {_, _, D}, _) when D rem 10 =:= 2 -> "nd";
+tag_to_value($S, {_, _, D}, _) when D rem 10 =:= 3 -> "rd";
+tag_to_value($S, _, _) -> "th";
+
+% Number of days in the given month; i.e. '28' to '31'
+tag_to_value($t, {Y,M,_}, _) ->
+   integer_to_list(calendar:last_day_of_the_month(Y,M));
+
+% Time zone of this machine; e.g. 'EST' or 'MDT'
+tag_to_value($T, _, _) ->
+   "TODO";
+
+% Seconds since the Unix epoch (January 1 1970 00:00:00 GMT)
+tag_to_value($U, Date, Time) ->
+    EpochSecs = calendar:datetime_to_gregorian_seconds({Date, Time})
+       - calendar:datetime_to_gregorian_seconds({{1970,1,1},{0,0,0}}),
+    integer_to_list(EpochSecs);
+
+% Day of the week, numeric, i.e. '0' (Sunday) to '6' (Saturday)
+tag_to_value($w, Date, _) ->
+   % Note: calendar:day_of_the_week returns
+   %   1 | .. | 7. Monday = 1, Tuesday = 2, ..., Sunday = 7
+   integer_to_list(calendar:day_of_the_week(Date) rem 7);
+
+% ISO-8601 week number of year, weeks starting on Monday
+tag_to_value($W, {Y,M,D}, _) ->
+   integer_to_list(year_weeknum(Y,M,D));
+
+% Year, 2 digits; e.g. '99'
+tag_to_value($y, {Y, _, _}, _) ->
+   string:sub_string(integer_to_list(Y), 3);
+
+% Year, 4 digits; e.g. '1999'
+tag_to_value($Y, {Y, _, _}, _) ->
+   integer_to_list(Y);
+
+% Day of the year; i.e. '0' to '365'
+tag_to_value($z, {Y,M,D}, _) ->
+    integer_to_list(day_of_year(Y,M,D));
+
+% Time zone offset in seconds (i.e. '-43200' to '43200'). The offset for
+% timezones west of UTC is always negative, and for those east of UTC is
+% always positive.
+tag_to_value($Z, _, _) ->
+   "TODO";
+
+tag_to_value(C, Date, Time) ->
+    io:format("Unimplemented tag : ~p [Date : ~p] [Time : ~p]",
+        [C, Date, Time]),
+    "".
+
+% Date helper functions
+day_of_year(Y,M,D) ->
+   day_of_year(Y,M,D,0).
+day_of_year(_Y,M,D,Count) when M =< 1 ->
+   D + Count;
+day_of_year(Y,M,D,Count) when M =< 12 ->
+   day_of_year(Y, M - 1, D, Count + calendar:last_day_of_the_month(Y,M));
+day_of_year(Y,_M,D,_Count) ->
+   day_of_year(Y, 12, D, 0).
+
+hour_24to12(0) -> 12;
+hour_24to12(H) when H < 13 -> H;
+hour_24to12(H) when H < 24 -> H - 12;
+hour_24to12(H) -> H.
+
+year_weeknum(Y,M,D) -> 
+    First = (calendar:day_of_the_week(Y, 1, 1) rem 7) - 1,
+    Wk = ((((calendar:date_to_gregorian_days(Y, M, D) -
+            calendar:date_to_gregorian_days(Y, 1, 1)) + First) div 7)
+           + (case First < 4 of true -> 1; _ -> 0 end)),
+    case Wk of
+       0 -> weeks_in_year(Y - 1);
+       _ -> case weeks_in_year(Y) of
+              WksInThisYear when Wk > WksInThisYear -> 1;
+              _ -> Wk
+            end
+    end.
+   
+weeks_in_year(Y) ->
+    D1 = calendar:day_of_the_week(Y, 1, 1),
+    D2 = calendar:day_of_the_week(Y, 12, 31),
+    if (D1 =:= 4 orelse D2 =:= 4) -> 53; true -> 52 end.
+
+utc_diff(Date, Time) ->
+   % Note: this code is plagarised from yaws_log
+   UTime = erlang:universaltime(),
+   DiffSecs = calendar:datetime_to_gregorian_seconds({Date,Time})
+       - calendar:datetime_to_gregorian_seconds(UTime),
+   ((DiffSecs/3600)*100).
+
+dayname(1) -> "monday";
+dayname(2) -> "tuesday";
+dayname(3) -> "wednesday";
+dayname(4) -> "thursday";
+dayname(5) -> "friday";
+dayname(6) -> "saturday";
+dayname(7) -> "sunday";
+dayname(_) -> "???".
+
+monthname(1) ->  "january";
+monthname(2) ->  "february";
+monthname(3) ->  "march";
+monthname(4) ->  "april";
+monthname(5) ->  "may";
+monthname(6) ->  "june";
+monthname(7) ->  "july";
+monthname(8) ->  "august";
+monthname(9) ->  "september";
+monthname(10) -> "october";
+monthname(11) -> "november";
+monthname(12) -> "december";
+monthname(_) -> "???".
+
+% Utility functions
+integer_to_list_zerofill(N) when N < 10 ->
+    lists:flatten(io_lib:format("~2..0B", [N]));
+integer_to_list_zerofill(N) ->
+    integer_to_list(N).
+
+ucfirst([First | Rest]) when First >= $a, First =< $z ->
+    [First-($a-$A) | Rest];
+ucfirst(Other) ->
+    Other.
+
+

+ 6 - 0
src/erlydtl/erlydtl_compiler.erl

@@ -298,6 +298,12 @@ body_ast(DjangoParseTree, Context, TreeWalker) ->
                 body_ast(Block, Context, TreeWalkerAcc);
                 body_ast(Block, Context, TreeWalkerAcc);
             ({'comment', _Contents}, TreeWalkerAcc) ->
             ({'comment', _Contents}, TreeWalkerAcc) ->
                 empty_ast(TreeWalkerAcc);
                 empty_ast(TreeWalkerAcc);
+            ({'date', 'now', {string_literal, _Pos, FormatString}}, TreeWalkerAcc) ->
+                % Note: we can't use unescape_string_literal here
+                % because we want to allow escaping in the format string.
+                % We only want to remove the surrounding quotes, i.e. \"foo\"
+                Unquoted = string:sub_string(FormatString, 2, length(FormatString) - 1),
+                string_ast(dateformat:format(Unquoted), TreeWalkerAcc);
             ({'autoescape', {identifier, _, OnOrOff}, Contents}, TreeWalkerAcc) ->
             ({'autoescape', {identifier, _, OnOrOff}, Contents}, TreeWalkerAcc) ->
                 body_ast(Contents, Context#dtl_context{auto_escape = list_to_atom(OnOrOff)}, 
                 body_ast(Contents, Context#dtl_context{auto_escape = list_to_atom(OnOrOff)}, 
                     TreeWalkerAcc);
                     TreeWalkerAcc);

+ 4 - 0
src/erlydtl/erlydtl_parser.yrl

@@ -40,6 +40,7 @@ Nonterminals
 
 
     ExtendsTag
     ExtendsTag
     IncludeTag
     IncludeTag
+    NowTag
 
 
     BlockBlock
     BlockBlock
     BlockBraced
     BlockBraced
@@ -117,6 +118,7 @@ Terminals
     include_keyword
     include_keyword
     load_keyword
     load_keyword
     not_keyword
     not_keyword
+    now_keyword
     number_literal
     number_literal
     open_tag
     open_tag
     open_var
     open_var
@@ -133,6 +135,7 @@ Elements -> Elements text : '$1' ++ ['$2'].
 Elements -> Elements ValueBraced : '$1' ++ ['$2'].
 Elements -> Elements ValueBraced : '$1' ++ ['$2'].
 Elements -> Elements ExtendsTag : '$1' ++ ['$2'].
 Elements -> Elements ExtendsTag : '$1' ++ ['$2'].
 Elements -> Elements IncludeTag : '$1' ++ ['$2'].
 Elements -> Elements IncludeTag : '$1' ++ ['$2'].
+Elements -> Elements NowTag : '$1' ++ ['$2'].
 Elements -> Elements LoadTag : '$1' ++ ['$2'].
 Elements -> Elements LoadTag : '$1' ++ ['$2'].
 Elements -> Elements BlockBlock : '$1' ++ ['$2'].
 Elements -> Elements BlockBlock : '$1' ++ ['$2'].
 Elements -> Elements ForBlock : '$1' ++ ['$2'].
 Elements -> Elements ForBlock : '$1' ++ ['$2'].
@@ -156,6 +159,7 @@ Variable -> Value dot identifier : {attribute, {'$3', '$1'}}.
 
 
 ExtendsTag -> open_tag extends_keyword string_literal close_tag : {extends, '$3'}.
 ExtendsTag -> open_tag extends_keyword string_literal close_tag : {extends, '$3'}.
 IncludeTag -> open_tag include_keyword string_literal close_tag : {include, '$3'}.
 IncludeTag -> open_tag include_keyword string_literal close_tag : {include, '$3'}.
+NowTag -> open_tag now_keyword string_literal close_tag : {date, now, '$3'}.
 
 
 LoadTag -> open_tag load_keyword LoadNames close_tag : {load, '$3'}.
 LoadTag -> open_tag load_keyword LoadNames close_tag : {load, '$3'}.
 LoadNames -> identifier : ['$1'].
 LoadNames -> identifier : ['$1'].

+ 189 - 0
src/tests/erlydtl_dateformat_tests.erl

@@ -0,0 +1,189 @@
+-module(erlydtl_dateformat_tests).
+
+-export([run_tests/0]).
+
+-define(DISPLAY_PASSES, false).
+
+run_tests() ->
+   test_group_runner([
+      {
+         "date 1",
+         {1979, 7, 8}, % just a date
+         [{"a", "a.m."}, {"A", "AM"}, {"d", "08"},
+          {"D", "Sun"}, {"f", "12"}, {"F", "July"},
+          {"g", "12"}, {"G", "0"},
+          {"j", "8"}, {"l", "Sunday"}, {"L", "False"},
+          {"m", "07"}, {"M", "Jul"}, {"b", "jul"},
+          {"n", "7"}, {"N", "July"}, {"P", "midnight"},
+          {"s", "00"}, {"S", "th"}, {"t", "31"},
+          {"w", "0"}, {"W", "27"}, {"y", "79"}, {"z", "189"},
+          {"r", "Sun, 8 Jul 1979 00:00:00 +0000"},
+          {"jS F Y H:i", "8th July 1979 00:00"},
+          {"jS o\\f F", "8th of July"},
+          % We expect these to come back verbatim
+          {"x", "x"}, {"C", "C"}, {";", ";"}, {"%", "%"}
+
+          % TODO : timzeone related tests.
+          %{"O", "0000"},
+          %{"T", "CET"},
+          %{"U", "300531600"},
+          %{"Z", "3600"}
+         ]
+      },
+      {
+         "datetime 1",
+         {{1979, 7, 8}, {22, 7, 12}}, % date/time tuple
+         [{"a", "p.m."}, {"A", "PM"}, {"d", "08"},
+          {"D", "Sun"}, {"f", "10:07"}, {"F", "July"},
+          {"g", "10"}, {"G", "22"},
+          {"j", "8"}, {"l", "Sunday"}, {"L", "False"},
+          {"m", "07"}, {"M", "Jul"}, {"b", "jul"},
+          {"n", "7"}, {"N", "July"}, {"P", "10:07 p.m."},
+          {"s", "12"}, {"S", "th"}, {"t", "31"},
+          {"w", "0"}, {"W", "27"}, {"y", "79"}, {"z", "189"},
+          {"r", "Sun, 8 Jul 1979 22:07:12 +0000"},
+          {"jS F Y H:i", "8th July 1979 22:07"},
+          {"jS o\\f F", "8th of July"},
+          % We expect these to come back verbatim
+          {"x", "x"}, {"C", "C"}, {";", ";"}, {"%", "%"}
+          % TODO : timzeone related tests.
+          %{"O", "0000"},
+          %{"T", "CET"},
+          %{"U", "300531600"},
+          %{"Z", "3600"}
+         ]
+      },
+      {
+         "datetime 2",
+         {{2008, 12, 25}, {7, 0, 9}}, % date/time tuple
+         [{"a", "a.m."}, {"A", "AM"}, {"d", "25"},
+          {"D", "Thu"}, {"f", "7"}, {"F", "December"},
+          {"g", "7"}, {"G", "7"},
+          {"j", "25"}, {"l", "Thursday"}, {"L", "True"},
+          {"m", "12"}, {"M", "Dec"}, {"b", "dec"},
+          {"n", "12"}, {"N", "Dec."}, {"P", "7 a.m."},
+          {"s", "09"}, {"S", "th"}, {"t", "31"},
+          {"w", "4"}, {"W", "52"}, {"y", "08"}, {"z", "360"},
+          {"r", "Thu, 25 Dec 2008 07:00:09 +0000"},
+          {"jS F Y H:i", "25th December 2008 07:00"},
+          {"jS o\\f F", "25th of December"},
+          % We expect these to come back verbatim
+          {"x", "x"}, {"C", "C"}, {";", ";"}, {"%", "%"}
+          % TODO : timzeone related tests.
+          %{"O", "0000"},
+          %{"T", "CET"},
+          %{"U", "300531600"},
+          %{"Z", "3600"}
+         ]
+      },
+      {
+         "datetime 3",
+         {{2004, 2, 29}, {12, 0, 59}}, % date/time tuple
+         [{"a", "p.m."}, {"A", "PM"}, {"d", "29"},
+          {"D", "Sun"}, {"f", "12"}, {"F", "February"},
+          {"g", "12"}, {"G", "12"},
+          {"j", "29"}, {"l", "Sunday"}, {"L", "True"},
+          {"m", "02"}, {"M", "Feb"}, {"b", "feb"},
+          {"n", "2"}, {"N", "Feb."}, {"P", "noon"},
+          {"s", "59"}, {"S", "th"}, {"t", "29"},
+          {"w", "0"}, {"W", "9"}, {"y", "04"}, {"z", "58"},
+          {"r", "Sun, 29 Feb 2004 12:00:59 +0000"},
+          {"jS F Y H:i", "29th February 2004 12:00"},
+          {"jS o\\f F", "29th of February"},
+          % We expect these to come back verbatim
+          {"x", "x"}, {"C", "C"}, {";", ";"}, {"%", "%"}
+          % TODO : timzeone related tests.
+          %{"O", "0000"},
+          %{"T", "CET"},
+          %{"U", "300531600"},
+          %{"Z", "3600"}
+         ]
+      },
+      % Weeknum tests.  Largely based on examples from :
+      %   http://en.wikipedia.org/wiki/ISO_week_date
+      { "weeknum 1.1",  {2005,  1,  1}, [{"W", "53"}] },
+      { "weeknum 1.2",  {2005,  1,  2}, [{"W", "53"}] },
+      { "weeknum 1.3",  {2005, 12, 31}, [{"W", "52"}] },
+      { "weeknum 1.4",  {2007,  1,  1}, [{"W", "1"}]  },
+      { "weeknum 1.5",  {2007, 12, 30}, [{"W", "52"}] },
+      { "weeknum 1.6",  {2007, 12, 31}, [{"W", "1"}]  },
+      { "weeknum 1.6",  {2008,  1,  1}, [{"W", "1"}]  },
+      { "weeknum 1.7",  {2008, 12, 29}, [{"W", "1"}]  },
+      { "weeknum 1.8",  {2008, 12, 31}, [{"W", "1"}]  },
+      { "weeknum 1.9",  {2009,  1,  1}, [{"W", "1"}]  },
+      { "weeknum 1.10", {2009, 12, 31}, [{"W", "53"}] },
+      { "weeknum 1.11", {2010,  1,  3}, [{"W", "53"}] },
+      % Examples where the ISO year is three days into
+      % the next Gregorian year
+      { "weeknum 2.1",  {2009, 12, 31}, [{"W", "53"}] },
+      { "weeknum 2.2",  {2010,  1,  1}, [{"W", "53"}] },
+      { "weeknum 2.3",  {2010,  1,  2}, [{"W", "53"}] },
+      { "weeknum 2.4",  {2010,  1,  3}, [{"W", "53"}] },
+      { "weeknum 2.5",  {2010,  1,  5}, [{"W", "1"}] },
+      % Example where the ISO year is three days into
+      % the previous Gregorian year
+      { "weeknum 3.1",  {2008, 12, 28}, [{"W", "52"}] },
+      { "weeknum 3.2",  {2008, 12, 29}, [{"W", "1"}] },
+      { "weeknum 3.3",  {2008, 12, 30}, [{"W", "1"}] },
+      { "weeknum 3.4",  {2008, 12, 31}, [{"W", "1"}] },
+      { "weeknum 3.5",  {2009,  1,  1}, [{"W", "1"}] },
+      % freeform tests
+      { "weeknum 4.1",  {2008,  2, 28}, [{"W", "9"}] },
+      { "weeknum 4.2",  {1975,  7, 24}, [{"W","30"}] },
+
+      % Ordinal suffix tests.
+      { "Ordinal suffix 1", {1984,1,1},  [{"S", "st"}] },
+      { "Ordinal suffix 2", {1984,2,2},  [{"S", "nd"}] },
+      { "Ordinal suffix 3", {1984,3,3},  [{"S", "rd"}] },
+      { "Ordinal suffix 4", {1984,4,4},  [{"S", "th"}] },
+      { "Ordinal suffix 5", {1984,6,5},  [{"S", "th"}] },
+      { "Ordinal suffix 7", {1984,2,9},  [{"S", "th"}] },
+      { "Ordinal suffix 8", {1984,9,9},  [{"S", "th"}] },
+      { "Ordinal suffix 9", {1984,11,10}, [{"S", "th"}] },
+      { "Ordinal suffix 10", {1984,12,11}, [{"S", "th"}] },
+      { "Ordinal suffix 11", {1984,8,12}, [{"S", "th"}] },
+      { "Ordinal suffix 12", {1984,1,19}, [{"S", "th"}] },
+      { "Ordinal suffix 13", {1984,2,20}, [{"S", "th"}] },
+      { "Ordinal suffix 14", {1984,2,21}, [{"S", "st"}] },
+      { "Ordinal suffix 15", {1984,7,22}, [{"S", "nd"}] },
+      { "Ordinal suffix 16", {1984,6,23}, [{"S", "rd"}] },
+      { "Ordinal suffix 17", {1984,5,24}, [{"S", "th"}] },
+      { "Ordinal suffix 18", {1984,1,29}, [{"S", "th"}] },
+      { "Ordinal suffix 19", {1984,3,30}, [{"S", "th"}] },
+      { "Ordinal suffix 20", {1984,1,31}, [{"S", "st"}] },
+      { "Ordinal suffix 21", {1984,1,310}, [{"S", "th"}] },
+      { "Ordinal suffix 22", {1984,1,121}, [{"S", "st"}] }
+   ]),
+
+   ok.
+
+test_group_runner([]) -> ok;
+test_group_runner([{Info, DateParam, Tests} | Rest]) ->
+   io:format("Running ~p -> ", [Info]),
+   PassCount = test_runner(DateParam, Tests),
+   case PassCount =:= length(Tests) of
+       true ->
+           io:format("Passed ~p/~p~n", [PassCount, length(Tests)]);
+       _ ->
+           io:format("~nFailed ~p/~p~n", [length(Tests) - PassCount, length(Tests)])
+   end,
+   test_group_runner(Rest).
+
+test_runner(DateParam, Tests) ->
+    test_runner(DateParam, Tests, 1, 0).
+test_runner(_DateParam, [], _TestNum, PassCount) ->
+    PassCount;
+test_runner(DateParam, [{Input, Expect} | Rest], TestNum, PassCount) ->
+    Text = "'" ++ Input ++ "' -> '" ++ Expect ++ "'",
+    IsPass = is(TestNum, Text, dateformat:format(DateParam, Input), Expect),
+    test_runner(DateParam, Rest, TestNum + 1, PassCount + IsPass).
+    
+is(TestNum, Text, Input1, Input2) when Input1 =:= Input2, ?DISPLAY_PASSES ->
+    io:format("~nok ~p - ~s", [TestNum, Text]),
+    1;
+is(_TestNum, _Text, Input1, Input2) when Input1 =:= Input2 ->
+    1;
+is(TestNum, Text, Input1, Input2) -> 
+    io:format("~nnot ok ~p - ~s~n     got : ~p~n expcted : ~p", [
+       TestNum, Text, Input1, Input2]),
+    0.

+ 5 - 1
src/tests/erlydtl_functional_tests.erl

@@ -124,6 +124,10 @@ setup("ifnotequal_preset") ->
     CompileVars = [{var1, "foo"}, {var2, "foo"}],
     CompileVars = [{var1, "foo"}, {var2, "foo"}],
     RenderVars = [],
     RenderVars = [],
     {ok, CompileVars, ok, RenderVars}; 
     {ok, CompileVars, ok, RenderVars}; 
+setup("now") ->
+    CompileVars = [],
+    RenderVars = [],
+    {ok, CompileVars, ok, RenderVars}; 
 setup("var") ->
 setup("var") ->
     CompileVars = [],
     CompileVars = [],
     RenderVars = [{var1, "foostring1"}, {var2, "foostring2"}, {var_not_used, "foostring3"}],
     RenderVars = [{var1, "foostring1"}, {var2, "foostring2"}, {var_not_used, "foostring3"}],
@@ -282,4 +286,4 @@ templates_docroot() ->
     filename:join([erlydtl_deps:get_base_dir(), "examples", "docroot"]).
     filename:join([erlydtl_deps:get_base_dir(), "examples", "docroot"]).
 
 
 templates_outdir() ->   
 templates_outdir() ->   
-    filename:join([erlydtl_deps:get_base_dir(), "examples", "rendered_output"]).
+    filename:join([erlydtl_deps:get_base_dir(), "examples", "rendered_output"]).

+ 25 - 0
src/tests/erlydtl_unittests.erl

@@ -49,6 +49,10 @@ tests() ->
                     <<"{{ person.city.state.country }}">>, [{person, [{city, [{state, [{country, "Italy"}]}]}]}],
                     <<"{{ person.city.state.country }}">>, [{person, [{city, [{state, [{country, "Italy"}]}]}]}],
                     <<"Italy">>}
                     <<"Italy">>}
             ]},
             ]},
+        {"now", [
+               {"now functional",
+                  <<"It is the {% now \"jS o\\f F Y\" %}.">>, [{var1, ""}], generate_test_date()}
+            ]},
         {"if", [
         {"if", [
                 {"If/else",
                 {"If/else",
                     <<"{% if var1 %}boo{% else %}yay{% endif %}">>, [{var1, ""}], <<"yay">>},
                     <<"{% if var1 %}boo{% else %}yay{% endif %}">>, [{var1, ""}], <<"yay">>},
@@ -276,3 +280,24 @@ vars_to_binary(Vars) when is_list(Vars) ->
         end, Vars);
         end, Vars);
 vars_to_binary(Vars) ->
 vars_to_binary(Vars) ->
     Vars.
     Vars.
+
+generate_test_date() ->
+    {{Y,M,D}, _} = erlang:localtime(),
+    MonthName = [
+       "January", "February", "March", "April",
+       "May", "June", "July", "August", "September",
+       "October", "November", "December"
+    ],
+    OrdinalSuffix = [
+       "st","nd","rd","th","th","th","th","th","th","th", % 1-10
+       "th","th","th","th","th","th","th","th","th","th", % 10-20
+       "st","nd","rd","th","th","th","th","th","th","th", % 20-30
+       "st"
+    ],
+    list_to_binary([
+         "It is the ",
+         integer_to_list(D),
+         lists:nth(D, OrdinalSuffix),
+         " of ", lists:nth(M, MonthName),
+         " ", integer_to_list(Y), "."
+    ]).