Re: [PATCH v3 2/9] parse-time-string: add a date/time parser to notmuch
authorMichal Nazarewicz <mina86@mina86.com>
Mon, 17 Sep 2012 14:13:35 +0000 (16:13 +0200)
committerW. Trevor King <wking@tremily.us>
Fri, 7 Nov 2014 17:49:32 +0000 (09:49 -0800)
de/7a847d5b3a15e0566cd2273327edd7606056af [new file with mode: 0644]

diff --git a/de/7a847d5b3a15e0566cd2273327edd7606056af b/de/7a847d5b3a15e0566cd2273327edd7606056af
new file mode 100644 (file)
index 0000000..b103dfd
--- /dev/null
@@ -0,0 +1,1086 @@
+Return-Path: <mpn@google.com>\r
+X-Original-To: notmuch@notmuchmail.org\r
+Delivered-To: notmuch@notmuchmail.org\r
+Received: from localhost (localhost [127.0.0.1])\r
+       by olra.theworths.org (Postfix) with ESMTP id 4831C431FAF\r
+       for <notmuch@notmuchmail.org>; Mon, 17 Sep 2012 07:13:51 -0700 (PDT)\r
+X-Virus-Scanned: Debian amavisd-new at olra.theworths.org\r
+X-Spam-Flag: NO\r
+X-Spam-Score: -0.7\r
+X-Spam-Level: \r
+X-Spam-Status: No, score=-0.7 tagged_above=-999 required=5\r
+       tests=[DKIM_SIGNED=0.1, DKIM_VALID=-0.1, RCVD_IN_DNSWL_LOW=-0.7]\r
+       autolearn=disabled\r
+Received: from olra.theworths.org ([127.0.0.1])\r
+       by localhost (olra.theworths.org [127.0.0.1]) (amavisd-new, port 10024)\r
+       with ESMTP id Fw+F4127W2uw for <notmuch@notmuchmail.org>;\r
+       Mon, 17 Sep 2012 07:13:48 -0700 (PDT)\r
+Received: from mail-bk0-f53.google.com (mail-bk0-f53.google.com\r
+       [209.85.214.53]) (using TLSv1 with cipher RC4-SHA (128/128 bits))\r
+       (No client certificate requested)\r
+       by olra.theworths.org (Postfix) with ESMTPS id E5754431FAE\r
+       for <notmuch@notmuchmail.org>; Mon, 17 Sep 2012 07:13:47 -0700 (PDT)\r
+Received: by bkwj4 with SMTP id j4so2779763bkw.26\r
+       for <notmuch@notmuchmail.org>; Mon, 17 Sep 2012 07:13:45 -0700 (PDT)\r
+DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com;\r
+ s=20120113;   h=sender:from:to:subject:in-reply-to:organization:references\r
+       :user-agent:x-face:face:x-pgp:x-pgp-fp:date:message-id:mime-version\r
+       :content-type; bh=SXKUsmwu/XziMg4brNjKTGDYW8B15ZqO0UtdkTxkpPg=;\r
+       b=pLAHGftO/0ZKngdQR98x5OteyD04pbkPE59nLF2fQ3h76SUlNmHX5sjZQJwyx6s2pF\r
+       ad35K6xDG+73CclcRlTNLsbFXiTJvW9hfGIPUugxCwnOTOqhdK2asJvo4roNfFl+quI6\r
+       22rGZnbPVbY4xk6s3EodUJb1fp7cp307dxCWlhQq+FIFLfBF5sKriaD3Vov0K17mjym0\r
+       aXzird+Y149jm70qyev9OiWphi2TuSdkJNKX9FNqUBZE7IXwwXm6LqlBjbXNiweYQ5AL\r
+       RnxZiUGRnlaqEfNOVVK4PprlD61UTWBjO/LNnsOBvNVwk3qUpTmbHeual8eAX7QLq1PS    /baw==\r
+X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;\r
+       d=google.com; s=20120113;\r
+       h=sender:from:to:subject:in-reply-to:organization:references\r
+       :user-agent:x-face:face:x-pgp:x-pgp-fp:date:message-id:mime-version\r
+       :content-type:x-gm-message-state;\r
+       bh=SXKUsmwu/XziMg4brNjKTGDYW8B15ZqO0UtdkTxkpPg=;\r
+       b=kGbgTWuWqje8sC096jaqF5rLNOd+lXV5wVegczAmw9cTMtOtfnOwT8AOqOf55IfC9M\r
+       d47g8Cvg7TGhhXa2/v9uY0v4hfon224VVfp23p17xX/AKEqXqvmOLmRBVTgvvCHIjkYB\r
+       ZGJwFW9yADj0MyM3aDq7mm2heFzuzelu6RLLGBtVjqHuivJnxbjQDs5N6V+/UV/WykXa\r
+       ygRbJii0hjpoMpLvww7EXUFS7/hY6+G12XQ59rQyJ1Bb6yII5ri4CaESxlFjWrcNKhCD\r
+       fe/ZvnLsdsbYglRrM35OlkaYlQC4bpYTDdt7VsnOsTmKRbxCr7tqtyXWRAxowvcvKWv6\r
+       YmLg==\r
+Received: by 10.204.148.83 with SMTP id o19mr4351950bkv.74.1347891225206;\r
+       Mon, 17 Sep 2012 07:13:45 -0700 (PDT)\r
+Received: by 10.204.148.83 with SMTP id o19mr4351935bkv.74.1347891224863;\r
+       Mon, 17 Sep 2012 07:13:44 -0700 (PDT)\r
+Received: from mpn-glaptop ([172.28.91.186])\r
+       by mx.google.com with ESMTPS id 14sm5629228bkw.15.2012.09.17.07.13.41\r
+       (version=TLSv1/SSLv3 cipher=OTHER);\r
+       Mon, 17 Sep 2012 07:13:43 -0700 (PDT)\r
+Sender: Michal Nazarewicz <mpn@google.com>\r
+From: Michal Nazarewicz <mina86@mina86.com>\r
+To: Jani Nikula <jani@nikula.org>, notmuch@notmuchmail.org,\r
+       David Bremner <david@tethera.net>\r
+Subject: Re: [PATCH v3 2/9] parse-time-string: add a date/time parser to\r
+       notmuch\r
+In-Reply-To: <878vcerurc.fsf@nikula.org>\r
+Organization: http://mina86.com/\r
+References: <cover.1347484177.git.jani@nikula.org>\r
+       <89741ec9a9687fca8b30aa1a4877392d355dd3ce.1347484177.git.jani@nikula.org>\r
+       <xa1tvcfidvpe.fsf@mina86.com> <878vcerurc.fsf@nikula.org>\r
+User-Agent: Notmuch/0.14+22~g8bdc16b (http://notmuchmail.org) Emacs/24.2.50.1\r
+       (x86_64-unknown-linux-gnu)\r
+X-Face: PbkBB1w#)bOqd`iCe"Ds{e+!C7`pkC9a|f)Qo^BMQvy\q5x3?vDQJeN(DS?|-^$uMti[3D*#^_Ts"pU$jBQLq~Ud6iNwAw_r_o_4]|JO?]}P_}Nc&"p#D(ZgUb4uCNPe7~a[DbPG0T~!&c.y$Ur,=N4RT>]dNpd;       KFrfMCylc}gc??'U2j,!8%xdD\r
+Face: iVBORw0KGgoAAAANSUhEUgAAADAAAAAwBAMAAAClLOS0AAAAJFBMVEWbfGlUPDDHgE57V0jUupKjgIObY0PLrom9mH4dFRK4gmjPs41MxjOgAAACQElEQVQ4jW3TMWvbQBQHcBk1xE6WyALX1069oZBMlq+ouUwpEQQ6uRjttkWP4CmBgGM0BQLBdPFZYPsyFUo6uEtKDQ7oy/U96XR2Ux8ehH/89Z6enqxBcS7Lg81jmSuujrfCZcLI/TYYvbGj+jbgFpHJ/bqQAUISj8iLyu4LuFHJTosxsucO4jSDNE0Hq3hwK/ceQ5sx97b8LcUDsILfk+ovHkOIsMbBfg43VuQ5Ln9YAGCkUdKJoXR9EclFBhixy3EGVz1K6eEkhxCAkeMMnqoAhAKwhoUJkDrCqvbecaYINlFKSRS1i12VKH1XpUd4qxL876EkMcDvHj3s5RBajHHMlA5iK32e0C7VgG0RlzFPvoYHZLRmAC0BmNcBruhkE0KsMsbEc62ZwUJDxWUdMsMhVqovoT96i/DnX/ASvz/6hbCabELLk/6FF/8PNpPCGqcZTGFcBhhAaZZDbQPaAB3+KrWWy2XgbYDNIinkdWAFcCpraDE/knwe5DBqGmgzESl1p2E4MWAz0VUPgYYzmfWb9yS4vCvgsxJriNTHoIBz5YteBvg+VGISQWUqhMiByPIPpygeDBE6elD973xWwKkEiHZAHKjhuPsFnBuArrzxtakRcISv+XMIPl4aGBUJm8Emk7qBYU8IlgNEIpiJhk/No24jHwkKTFHDWfPniR4iw5vJaw2nzSjfq2zffcE/GDjRC2dn0J0XwPAbDL84TvaFCJEU4Oml9pRyEUhR3Cl2t01AoEjRbs0sYugp14/4X5n4pU4EHHnMAAAAAElFTkSuQmCC\r
+X-PGP: 50751FF4\r
+X-PGP-FP: AC1F 5F5C D418 88F8 CC84 5858 2060 4012 5075 1FF4\r
+Date: Mon, 17 Sep 2012 16:13:35 +0200\r
+Message-ID: <xa1tpq5kzqi8.fsf@mina86.com>\r
+MIME-Version: 1.0\r
+Content-Type: multipart/mixed; boundary="=-=-="\r
+X-Gm-Message-State: ALoCoQlLV3rFEX4IG9wEvoVab+ezDzAM8zmuD17sqgf1ybc6TCmgyQFx8l1ChUNjRZeaVXQNmaS1v0n/3G+F109dmqrBUKdT2ejBkVH2TpXKvY1vBYySU+bW2mZeImXtGJVDJ3juu6Uq5D48AlAQ7SQdi6oFC+2j+WWA37XDrT1V/VW7z7cUhG4KBRF47sAZV7ucsT2Xe/uF\r
+X-BeenThere: notmuch@notmuchmail.org\r
+X-Mailman-Version: 2.1.13\r
+Precedence: list\r
+List-Id: "Use and development of the notmuch mail system."\r
+       <notmuch.notmuchmail.org>\r
+List-Unsubscribe: <http://notmuchmail.org/mailman/options/notmuch>,\r
+       <mailto:notmuch-request@notmuchmail.org?subject=unsubscribe>\r
+List-Archive: <http://notmuchmail.org/pipermail/notmuch>\r
+List-Post: <mailto:notmuch@notmuchmail.org>\r
+List-Help: <mailto:notmuch-request@notmuchmail.org?subject=help>\r
+List-Subscribe: <http://notmuchmail.org/mailman/listinfo/notmuch>,\r
+       <mailto:notmuch-request@notmuchmail.org?subject=subscribe>\r
+X-List-Received-Date: Mon, 17 Sep 2012 14:13:51 -0000\r
+\r
+--=-=-=\r
+Content-Type: text/plain; charset=utf-8\r
+Content-Transfer-Encoding: quoted-printable\r
+\r
+> On Thu, 13 Sep 2012, Michal Nazarewicz <mina86@mina86.com> wrote:\r
+>> Have you consider doing the same in bison?  I consider the code totally\r
+>> unreadable and unmaintainable.\r
+\r
+On Thu, Sep 13 2012, Jani Nikula wrote:\r
+> I do not think you could easily do everything that this parser does in\r
+> bison. But then I'm not an expert in bison, and I have zero ambition to\r
+> become one. So I'm biased, and I'm open about it.\r
+\r
+Bison can do a lot of weird stuff including modifying how lexer\r
+interpretes tokens even while parsing given grammar rule.\r
+\r
+> Even so, if you're suggesting doing this in bison would make this\r
+> totally readable and maintainable, I urge you to have a good look at\r
+> [1]. Note that it also does less in more lines of code. (And using it\r
+> as-is in notmuch has pretty much been turned down in the past.)\r
+>\r
+> Finally, I also suggest you actually read and review the code, pointing\r
+> out concrete issues in readability or maintainability that you\r
+> see. Especially since an earlier version has received comment "[I]t\r
+> looks very nice to me. It is pleasantly nice to read." [2]. What you're\r
+> doing is worthless bikeshedding otherwise.\r
+\r
+I'm sorry.  I sometime tend to go into extremes with my statements, so\r
+yes, the =E2=80=9Ctotally unreadable=E2=80=9D was a over statement on my pa=\r
+rt.\r
+\r
+My point was however that parsing is a solved problem, and for\r
+non-trivial parsers one needs to ask herself whether it's worth trying\r
+to implement the logic, or maybe using a parser generator is just\r
+simpler.\r
+\r
+And in this particular case, my feeling is that bison is easier to read\r
+and modify.\r
+\r
+To add some merit to my statement, I attach a bison parser.\r
+\r
+It supports ranges as so:\r
+       <date>          the specific moment with duration dependent\r
+                       on specification.  How duration is figured out\r
+                       is described in the next paragraph.\r
+       <from>..<to>    dates >=3D <from> and < <to>, so for instance\r
+                       =E2=80=9Cyesterday..0=E2=80=9D days yields results from yesterday.\r
+       ..<to>          dates < <to>\r
+       <from>..        dates >=3D <from>\r
+       <from>++<dur>   a shorthand of =E2=80=9C<from>..<from> + <dur>=E2=80=9D.\r
+                       This is useful for things like: =E2=80=9C2012q1++2\r
+                       quarters=E2=80=9D which is equivalent to\r
+                       =E2=80=9C2012/01/01..2012/07/01=E2=80=9D, ie. the first two\r
+                       quarters of 2012.\r
+\r
+It supports specifications as:\r
+       '@' <num>\r
+               Raw timestamp.  It's duration is one second.\r
+\r
+       <num> (seconds | minutes | hours | days | weeks | fortnights) [ago]\r
+               moves the date by given number of units in the future or\r
+               in the past (if =E2=80=9Cago=E2=80=9D is given).  <num> can be preceded\r
+               by sign.\r
+\r
+               This specification's duration is whatever unit was used,\r
+               ie. one second, one minute, one hour, one day, one week\r
+               or one fortnight.  So =E2=80=9C7 days ago=E2=80=9D and =E2=80=9C1 week ag=\r
+o=E2=80=9D\r
+               specify the same moment, but they hay different\r
+               durations.\r
+\r
+       <num> (months | quarters | years) [ago]\r
+               Like above, but calendar months are used which do not\r
+               always have the same length.  If applying the offset\r
+               ends up with a day of the month out of range, the day\r
+               is capped to the last day of the month.\r
+\r
+       yesterday\r
+               Moves one day back. [*]  Note that because of [*] this\r
+               is not quivalent to =E2=80=9C-1day=E2=80=9D.\r
+       YYYY/MM/DD\r
+       YYYY-MM-DD\r
+       MM-DD-YYYY\r
+       DD Month YYYY\r
+       Month DD YYYY\r
+               Sets date accordingly. [*] =E2=80=9CMonth=E2=80=9D is a human readable\r
+               month name.\r
+       Month [DD] [YYYY]\r
+               If either day or year is missing, given component of the\r
+               date is not changed. [*]  Also, if day is missing, the\r
+               duration is set to one month rather than one day (but\r
+               see caveats described in [*]).\r
+       YYYY q Q\r
+               Sets date to the beginning of quarter Q, ie. =E2=80=9C2012q2=E2=80=9D is\r
+               roughly the same as =E2=80=9C2012/04/01=E2=80=9D. [*] Sets duration to\r
+               three moths but see caveats described in [*].\r
+\r
+       midnight | noon\r
+               Sets time to 0:00:00 and 12:00:00 respectively.   Has\r
+               duration of 1 hour.\r
+       HH:MM:SS [am | pm]\r
+       HH:MM    [am | pm]\r
+       HH       (am | pm)\r
+               Sets time accordingly with the part that is not\r
+               specified set to zero.  Duration depends on how many\r
+               components are missing, ie. =E2=80=9CHH (am|pm)=E2=80=9D has a duration of\r
+               on hour, =E2=80=9CHH:MM=E2=80=9D has a duration of one minute and\r
+               =E2=80=9CHH:MM:SS=E2=80=9D has a duration of one second.\r
+\r
+[*] Formats specifying the date will zero the time to midnight unless\r
+    the time has already been specified (ie. =E2=80=9Cyesterday=E2=80=9D is=\r
+ roughly the\r
+    same as =E2=80=9Cyesterday midnight=E2=80=9D, but =E2=80=9Cnoon yesterd=\r
+ay=E2=80=9D still keeps time\r
+    as noon.\r
+\r
+    Also, if the time has not been specified, those formats will set\r
+    duration to one day (with two exception), so =E2=80=9Cyesterday=E2=80=\r
+=9D has\r
+    a duration of one day, but =E2=80=9Cyesterday midnight=E2=80=9D, even t=\r
+hough it\r
+    specifies the same moment's beginning, has a duration of one hour.\r
+\r
+Purposly, I have not added support for MM/DD/YYYY or DD/MM/YYYY as well\r
+as two-digit years.  I feel this would only add confusion.\r
+\r
+---\r
+ .gitignore            |    3 +\r
+ Makefile              |   17 ++\r
+ date-parser-grammar.y |  173 ++++++++++++++++++\r
+ date-parser.c         |  476 +++++++++++++++++++++++++++++++++++++++++++++=\r
+++++\r
+ date-parser.h         |   59 ++++++\r
+ test.c                |   44 +++++\r
+ 6 files changed, 772 insertions(+), 0 deletions(-)\r
+ create mode 100644 .gitignore\r
+ create mode 100644 Makefile\r
+ create mode 100644 date-parser-grammar.y\r
+ create mode 100644 date-parser.c\r
+ create mode 100644 date-parser.h\r
+ create mode 100644 test.c\r
+\r
+diff --git a/.gitignore b/.gitignore\r
+new file mode 100644\r
+index 0000000..b73c782\r
+--- /dev/null\r
++++ b/.gitignore\r
+@@ -0,0 +1,3 @@\r
++test\r
++*.o\r
++date-parser-grammar.tab.*\r
+diff --git a/Makefile b/Makefile\r
+new file mode 100644\r
+index 0000000..8d95f71\r
+--- /dev/null\r
++++ b/Makefile\r
+@@ -0,0 +1,17 @@\r
++CFLAGS +=3D -std=3Dc99 -Wextra -Werror -pedantic\r
++\r
++test: test.o date-parser.o date-parser-grammar.tab.o\r
++test.o: test.c date-parser.h\r
++date-parser.o: date-parser.c date-parser.h\r
++date-parser.o: date-parser-grammar.tab.h\r
++\r
++date-parser-grammar.tab.c: date-parser-grammar.y\r
++      bison $<\r
++\r
++date-parser-grammar.tab.h: date-parser-grammar.tab.c\r
++date-parser-grammar.tab.o: date-parser-grammar.tab.c date-parser.h\r
++date-parser-grammar.tab.o: CPPFLAGS +=3D -Wno-unreachable-code\r
++\r
++clean:\r
++      rm -f date-parser-grammar.output date-parser-grammar.tab.* \\r
++              *.o test\r
+diff --git a/date-parser-grammar.y b/date-parser-grammar.y\r
+new file mode 100644\r
+index 0000000..38ddfca\r
+--- /dev/null\r
++++ b/date-parser-grammar.y\r
+@@ -0,0 +1,173 @@\r
++/* Date parser bison grammar file\r
++ * Copyright (c) 2012 Google Inc.\r
++ * Written by Michal Nazarewicz <mina86@mina86.com>\r
++ *\r
++ * This program is free software: you can redistribute it and/or modify\r
++ * it under the terms of the GNU General Public License as published by\r
++ * the Free Software Foundation, either version 3 of the License, or\r
++ * (at your option) any later version.\r
++ *\r
++ * This program is distributed in the hope that it will be useful,\r
++ * but WITHOUT ANY WARRANTY; without even the implied warranty of\r
++ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\r
++ * GNU General Public License for more details.\r
++ *\r
++ * You should have received a copy of the GNU General Public License\r
++ * along with this program.  If not, see http://www.gnu.org/licenses/ . */\r
++\r
++%code requires {\r
++\r
++#ifndef YYSTYPE\r
++#  define YYSTYPE long\r
++#endif\r
++#ifndef YYLTYPE\r
++# define YYLTYPE struct yylocation\r
++#endif\r
++\r
++#ifdef YYLLOC_DEFAULT\r
++#  undef YYLLOC_DEFAULT\r
++#endif\r
++#define YYLLOC_DEFAULT(Cur, Rhs, N) do {              \\r
++      if (N) {                                        \\r
++              (Cur).start =3D YYRHSLOC(Rhs, 1).start; \\r
++              (Cur).end   =3D YYRHSLOC(Rhs, N).end;   \\r
++      } else {                                        \\r
++              (Cur) =3D YYRHSLOC(Rhs, 0);             \\r
++      }                                               \\r
++} while (0)\r
++}\r
++\r
++%code{\r
++#include "date-parser-grammar.tab.h"\r
++#include "date-parser.h"\r
++\r
++#define ASSERT(cond, loc, message) do {                       \\r
++      if (!(cond)) {                                  \\r
++              parse_date_print_error(&loc, message);  \\r
++              YYERROR;                                \\r
++      }                                               \\r
++} while (0)\r
++}\r
++\r
++%locations\r
++%defines\r
++%error-verbose\r
++%define               api.pure\r
++\r
++%parse-param  {struct date *ret}\r
++%parse-param  {const char **inputp}\r
++%lex-param    {const char **inputp}\r
++\r
++%token        T_NUMBER        "<num>"         /* Always positive. */\r
++%token        T_NUMBER_4      "####"          /* Four digit number. */\r
++%token        T_AGO           "ago"\r
++/* Also used for minutes, hours, days and weeks. */\r
++%token        T_SECONDS       "seconds"\r
++/* Also used for quarters and years */\r
++%token        T_MONTHS        "months"\r
++%token        T_YESTERDAY     "yesterday"\r
++%token        T_AMPM          "am/pm"\r
++%token        T_HOUR          "<hour>"\r
++%token        T_MONTH         "<month>"\r
++\r
++%expect       3       /* Two shift/reduce conflicts caused by year_maybe, and onde\r
++               * caused by day_maybe. */\r
++\r
++%%\r
++      /* For backwards compatibility, just a number and nothing else\r
++       * is treated as timestamp */\r
++input : number { date_set_from_stamp(ret, $1) }\r
++      | date\r
++      ;\r
++\r
++date  : part\r
++      | date part\r
++      ;\r
++\r
++part  : integer "seconds" ago_maybe {\r
++              ASSERT(date_add_seconds(ret, $1 * $3, $2), @$,\r
++                     "offset ends up in date out of range")\r
++      }\r
++      | integer "months"  ago_maybe {\r
++              ASSERT(date_add_months(ret, $1 * $3, $2), @$,\r
++                     "offset ends up in date out of range")\r
++      }\r
++      | "yesterday" {\r
++              ASSERT(date_set_yesterday(ret), @$,\r
++                     "offset ends up in date out of range")\r
++      }\r
++\r
++      | '@' number                    { date_set_from_stamp(ret, $2) }\r
++      | "<hour>"                      { date_set_time(ret, $1, -1, -1, -1) }\r
++\r
++      /* HH:MM, HH:MM:SS, HH:MM am/pm, HH:MM:SS am/pm */\r
++      | number ':' number seconds_maybe ampm_maybe {\r
++              ASSERT(date_set_time(ret, $1, $3, $4, $5), @$,\r
++                     "invalid time")\r
++      }\r
++\r
++      | number "am/pm" {                      /* HH am/pm */\r
++              ASSERT(date_set_time(ret, $1, -1, -1, $2), @$, "invalid hour")\r
++      }\r
++\r
++      | "####" '/' "<num>" '/' "<num>" {      /* YYYY/MM/DD */\r
++              ASSERT(date_set_date(ret, $1, $3, $5), @$, "invalid date")\r
++      }\r
++      | "####" '-' "<num>" '-' "<num>" {      /* YYYY-MM-DD */\r
++              ASSERT(date_set_date(ret, $1, $3, $5), @$, "invalid date")\r
++      }\r
++      | "<num>" '-' "<num>" '-' "####" {      /* DD-MM-YYYY */\r
++              ASSERT(date_set_date(ret, $5, $3, $1), @$, "invalid date")\r
++      }\r
++      /* No MM/DD/YYYY or DD/MM/YYYY because it's confusing. */\r
++\r
++      | "<num>" "<month>" year_maybe {        /* 1 January 2012 */\r
++              ASSERT(date_set_date(ret, $3, $2, $1), @$, "invalid date")\r
++      }\r
++      | "<month>" day_maybe year_maybe {      /* January 1 2012 */\r
++              ASSERT(date_set_date(ret, $3, $1, $2), @$, "invalid date")\r
++      }\r
++\r
++      | "####" 'q' "<num>" {                  /* Quarter, 2012q1 */\r
++              ASSERT(date_set_quarter(ret, $1, $3), @$, "invalid quarter");\r
++      }\r
++      ;\r
++\r
++number        : "<num>"       { $$ =3D $1 }\r
++      | "####"        { $$ =3D $1 }\r
++      ;\r
++\r
++integer       :     number    { $$ =3D  $1 }\r
++      | '-' number    { $$ =3D -$2 }\r
++      ;\r
++\r
++ago_maybe\r
++      : /* empty */   { $$ =3D  1 }\r
++      | "ago"         { $$ =3D -1 }\r
++      ;\r
++\r
++seconds_maybe\r
++      : /* empty */   { $$ =3D -1 }\r
++      | ':' "<num>"   { $$ =3D $2 }\r
++      ;\r
++\r
++ampm_maybe\r
++      : /* empty */   { $$ =3D -1 }\r
++      | "am/pm"       { $$ =3D $$ }\r
++      /* For people who like writing "a.m." or "p.m." and since dot\r
++       * is ignored by the lexer (ie. it's treated just like white\r
++       * space), dot is lost. */\r
++      | 'a' 'm'       { $$ =3D  0 }\r
++      | 'p' 'm'       { $$ =3D  0 }\r
++      ;\r
++\r
++day_maybe\r
++      : /* empty */   { $$ =3D -1 }\r
++      | "<num>"       { $$ =3D $1 }\r
++      ;\r
++\r
++year_maybe\r
++      : /* empty */   { $$ =3D -1 }\r
++      | "####"        { $$ =3D $1 }\r
++      ;\r
++%%\r
+diff --git a/date-parser.c b/date-parser.c\r
+new file mode 100644\r
+index 0000000..c1701bd\r
+--- /dev/null\r
++++ b/date-parser.c\r
+@@ -0,0 +1,476 @@\r
++/* Date parser implementation file.\r
++ * Copyright (c) 2012 Google Inc.\r
++ * Written by Michal Nazarewicz <mina86@mina86.com>\r
++ *\r
++ * This program is free software: you can redistribute it and/or modify\r
++ * it under the terms of the GNU General Public License as published by\r
++ * the Free Software Foundation, either version 3 of the License, or\r
++ * (at your option) any later version.\r
++ *\r
++ * This program is distributed in the hope that it will be useful,\r
++ * but WITHOUT ANY WARRANTY; without even the implied warranty of\r
++ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\r
++ * GNU General Public License for more details.\r
++ *\r
++ * You should have received a copy of the GNU General Public License\r
++ * along with this program.  If not, see http://www.gnu.org/licenses/ . */\r
++\r
++#define _POSIX_C_SOURCE 1\r
++\r
++#include "date-parser.h"\r
++#include "date-parser-grammar.tab.h"\r
++\r
++#include <ctype.h>\r
++#include <errno.h>\r
++#include <stdlib.h>\r
++#include <string.h>\r
++#include <strings.h>\r
++\r
++\r
++static bool is_valid_year(int year) {\r
++      /* TODO: Get the actual time_t range. */\r
++      return year >=3D 1970 && year < 2037;\r
++}\r
++\r
++\r
++/***************************** Basic date helpers ************************=\r
+***/\r
++\r
++static int days_in_months[] =3D {\r
++      31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31\r
++};\r
++\r
++static inline int is_leap(int year) {\r
++      return year % 4 =3D=3D 0 && (year % 100 || year % 400 =3D=3D 0);\r
++}\r
++\r
++static inline int days_in_month(int year, int month) {\r
++      return days_in_months[month] + (month =3D=3D 1 ? is_leap(year) : 0);\r
++}\r
++\r
++static inline int days_in_year(int year) {\r
++      return 365 + is_leap(year);\r
++}\r
++\r
++static inline int min(int a, int b) {\r
++      return a < b ? a : b;\r
++}\r
++\r
++\r
++/****************************** Date manipulation ************************=\r
+***/\r
++\r
++struct date {\r
++      struct tm tm;\r
++      int dur_sec, dur_mon, has_time;\r
++};\r
++\r
++\r
++void date_set_from_stamp(struct date *ret, long stamp) {\r
++      time_t t =3D stamp;\r
++      localtime_r(&t, &ret->tm);\r
++      ret->dur_sec =3D 1;\r
++      ret->dur_mon =3D 0;\r
++      ret->has_time =3D 1;\r
++}\r
++\r
++static void date_set_to_now(struct date *ret) {\r
++      time_t t =3D time(NULL);\r
++      localtime_r(&t, &ret->tm);\r
++      ret->dur_sec =3D 1;\r
++      ret->dur_mon =3D 0;\r
++      ret->has_time =3D 0;\r
++}\r
++\r
++static void date_zero_time(struct date *ret, int dur_sec, int dur_mon) {\r
++      if (!ret->has_time) {\r
++              ret->tm.tm_hour =3D ret->tm.tm_min =3D ret->tm.tm_sec =3D 0;\r
++              ret->dur_sec =3D dur_sec;\r
++              ret->dur_mon =3D dur_mon;\r
++      }\r
++}\r
++\r
++bool date_add_seconds(struct date *ret, long num, long unit) {\r
++      time_t t =3D mktime(&ret->tm) + num * unit;\r
++      localtime_r(&t, &ret->tm);\r
++      ret->dur_sec =3D unit;\r
++      ret->dur_mon =3D 0;\r
++      return true; /* TODO add validation */\r
++}\r
++\r
++bool date_set_yesterday(struct date *ret) {\r
++      if (ret->tm.tm_mday !=3D 1) {\r
++              --ret->tm.tm_mday;\r
++      } else if (ret->tm.tm_mon) {\r
++              --ret->tm.tm_mon;\r
++              ret->tm.tm_mday =3D days_in_month(ret->tm.tm_year + 1900,\r
++                                              ret->tm.tm_mon);\r
++      } else if (is_valid_year(1900 + ret->tm.tm_year - 1)) {\r
++              --ret->tm.tm_year;\r
++              ret->tm.tm_mon =3D 11;\r
++              ret->tm.tm_mday =3D 31;\r
++      } else {\r
++              return false;\r
++      }\r
++      date_zero_time(ret, 24 * 3600, 0);\r
++      ret->tm.tm_isdst =3D -1;\r
++      return true;\r
++}\r
++\r
++bool date_add_months(struct date *ret, long num, long unit) {\r
++      long y;\r
++\r
++      y =3D ret->tm.tm_year + 1900;\r
++      num =3D num * unit + ret->tm.tm_mon;\r
++      if (num < 0) {\r
++              y -=3D -num / 12;\r
++              num =3D 11 - (-num % 12);\r
++      } else {\r
++              y +=3D num / 12;\r
++              num %=3D 12;\r
++      }\r
++      if (!is_valid_year(y)) {\r
++              return false;\r
++      }\r
++\r
++      ret->tm.tm_year =3D y - 1900;\r
++      ret->tm.tm_mon =3D num;\r
++      ret->tm.tm_mday =3D min(ret->tm.tm_mday,\r
++                            days_in_month(ret->tm.tm_year + 1900,\r
++                                          ret->tm.tm_mon));\r
++      if (!ret->has_time) {\r
++              ret->dur_sec =3D 0;\r
++              ret->dur_mon =3D unit;\r
++      }\r
++      ret->tm.tm_isdst =3D -1;\r
++      return true;\r
++}\r
++\r
++bool date_set_time(struct date *ret, long h, long m, long s, int ampm) {\r
++      if (m > 60 || s > 60 || h > 23) {\r
++              return false;\r
++      }\r
++\r
++      if (ampm !=3D -1) {\r
++              if (!h || h > 12) {\r
++                      return false;\r
++              }\r
++              if (h !=3D 12) {\r
++                      h +=3D ampm * 12;\r
++              } else if (ampm) {  /* 12 pm */\r
++                      h =3D 12;\r
++              } else {\r
++                      /* 12 am is 0 the next day, so adjust date */\r
++                      date_add_seconds(ret, 1, 24 * 3600);\r
++                      h =3D 0;\r
++              }\r
++      }\r
++\r
++      if (m =3D=3D -1) {\r
++              ret->dur_sec =3D 3600;\r
++              m =3D s =3D 0;\r
++      } else if (s =3D=3D -1) {\r
++              ret->dur_sec =3D 60;\r
++              s =3D 0;\r
++      } else {\r
++              ret->dur_sec =3D 1;\r
++      }\r
++      ret->dur_mon =3D 0;\r
++\r
++      ret->tm.tm_hour =3D h;\r
++      ret->tm.tm_min =3D m;\r
++      ret->tm.tm_sec =3D s;\r
++      ret->tm.tm_isdst =3D -1;\r
++\r
++      ret->has_time =3D 1;\r
++      return true;\r
++}\r
++\r
++bool date_set_date(struct date *ret, long y, long m, long d) {\r
++      if (y =3D=3D -1) {\r
++              y =3D ret->tm.tm_year + 1900;\r
++      } else if (!is_valid_year(y)) {\r
++              return false;\r
++      }\r
++      if (m < 1 || m > 12 ||\r
++          (d !=3D -1 && (d < 1 || d > days_in_month(y, m)))) {\r
++              return false;\r
++      }\r
++      ret->tm.tm_year =3D y - 1900;\r
++      ret->tm.tm_mon =3D m - 1;\r
++      ret->tm.tm_mday =3D d =3D=3D -1 ? 1 : d;\r
++      if (d =3D=3D -1) {\r
++              date_zero_time(ret, 0, 1);\r
++      } else {\r
++              date_zero_time(ret, 24 * 3600, 0);\r
++      }\r
++      ret->tm.tm_isdst =3D -1;\r
++      return true;\r
++}\r
++\r
++bool date_set_quarter(struct date *ret, long y, long q) {\r
++      if (!is_valid_year(y) || q < 1 || q > 4) {\r
++              return false;\r
++      }\r
++      ret->tm.tm_year =3D y - 1900;\r
++      ret->tm.tm_mon =3D (q - 1) * 3;\r
++      ret->tm.tm_mday =3D 1;\r
++      date_zero_time(ret, 0, 3);\r
++      ret->tm.tm_isdst =3D -1;\r
++      return true;\r
++}\r
++\r
++\r
++#define TOKEN(str, token, num) { str, sizeof(str) - 1, token, num }\r
++#define ABBR(len) { NULL, len, 0, 0 }\r
++\r
++static struct token {\r
++      const char *str;\r
++      size_t len;\r
++      int token;\r
++      long num;\r
++} tokens_array[] =3D {\r
++      TOKEN("ago", T_AGO, 0),\r
++\r
++      TOKEN("am", T_AMPM, 0),\r
++      TOKEN("pm", T_AMPM, 1),\r
++\r
++      TOKEN("seconds",    T_SECONDS,              1), ABBR(3), ABBR(6),\r
++      TOKEN("minutes",    T_SECONDS,             60), ABBR(3), ABBR(6),\r
++      TOKEN("hours",      T_SECONDS,           3600), ABBR(4), ABBR(1),\r
++      TOKEN("days",       T_SECONDS,      24 * 3600), ABBR(3), ABBR(1),\r
++      TOKEN("weeks",      T_SECONDS,  7 * 24 * 3600), ABBR(4),\r
++      TOKEN("fortnights", T_SECONDS, 14 * 24 * 3600), ABBR(9),\r
++\r
++      TOKEN("months",     T_MONTHS,  1), ABBR(5),\r
++      TOKEN("quarters",   T_MONTHS,  3), ABBR(7),\r
++      TOKEN("years",      T_MONTHS, 12), ABBR(4),\r
++\r
++      TOKEN("yesterday",  T_YESTERDAY, 0),\r
++\r
++      TOKEN("midnight",   T_HOUR,  0),\r
++      TOKEN("noon",       T_HOUR, 12),\r
++\r
++      TOKEN("january",    T_MONTH,  1), ABBR(3),\r
++      TOKEN("february",   T_MONTH,  2), ABBR(3),\r
++      TOKEN("march",      T_MONTH,  3), ABBR(3),\r
++      TOKEN("april",      T_MONTH,  4), ABBR(3),\r
++      TOKEN("may",        T_MONTH,  5),\r
++      TOKEN("june",       T_MONTH,  6), ABBR(3),\r
++      TOKEN("july",       T_MONTH,  7), ABBR(3),\r
++      TOKEN("august",     T_MONTH,  8), ABBR(3),\r
++      TOKEN("september",  T_MONTH,  9), ABBR(4), ABBR(3),\r
++      TOKEN("october",    T_MONTH, 10), ABBR(3),\r
++      TOKEN("november",   T_MONTH, 11), ABBR(3),\r
++      TOKEN("december",   T_MONTH, 12), ABBR(3),\r
++\r
++      { NULL, 0, 0, 0 },\r
++};\r
++\r
++#undef TOKEN\r
++#undef ABBR\r
++\r
++\r
++static struct token locale_tokens_array[2*12 + 1];\r
++static bool locale_tokens_populated =3D false;\r
++\r
++static void populate_locale_tokens(void) {\r
++      static const char *mon_formats[] =3D { "%b", "%B" };\r
++      static char locale_buffer[1024];\r
++\r
++      char *buf =3D locale_buffer, *end =3D buf + sizeof(locale_buffer);\r
++      struct token *out =3D locale_tokens_array;\r
++      struct tm tm;\r
++      unsigned i;\r
++\r
++      tm.tm_sec =3D 0;\r
++      tm.tm_min =3D 0;\r
++      tm.tm_hour =3D 0;\r
++      tm.tm_mday =3D 10;\r
++      tm.tm_year =3D 100;\r
++      tm.tm_isdst =3D 0;\r
++\r
++      for (tm.tm_mon =3D 0; tm.tm_mon < 12; ++tm.tm_mon) {\r
++              for (i =3D 0; i < 2; ++i) {\r
++                      out->len =3D strftime(buf, end - buf,\r
++                                          mon_formats[i], &tm);\r
++                      if (!out->len) {\r
++                              continue;\r
++                      }\r
++                      out->str =3D buf;\r
++                      buf +=3D out->len;\r
++                      out->token =3D T_MONTH;\r
++                      out->num =3D tm.tm_mon + 1;\r
++                      ++out;\r
++              }\r
++      }\r
++\r
++      out->len =3D 0;\r
++}\r
++\r
++static const struct token *find_token(const struct token *tk,\r
++                                    const char *str, size_t len) {\r
++      const struct token *ret;\r
++\r
++      for (; tk->len; ++tk) {\r
++              if (tk->str) {\r
++                      ret =3D tk;\r
++              }\r
++              if (tk->len =3D=3D len && !strncasecmp(str, ret->str, len)) {\r
++                      return ret;\r
++              }\r
++      }\r
++\r
++      return NULL;\r
++}\r
++\r
++\r
++/* Treat '_' and '.' as white space so that people don't have to quote\r
++ * the argument when specifying it on command line. */\r
++#define SKIP_WHITE_SPACE(ch) do {                             \\r
++      while (isspace(*ch) || *ch =3D=3D '_' || *ch =3D=3D '.') {      \\r
++              ++ch;                                           \\r
++      }                                                       \\r
++} while (0)\r
++\r
++\r
++int yylex(YYSTYPE *valp, struct yylocation *loc, const char **inputp) {\r
++      const char *ch =3D *inputp, *str;\r
++      const struct token *tk;\r
++\r
++      SKIP_WHITE_SPACE(ch);\r
++\r
++      /* End of data */\r
++      if (*ch =3D=3D 0) {\r
++              return EOF;\r
++      }\r
++\r
++      loc->start =3D ch;\r
++\r
++      /* Parse number */\r
++      if (isdigit(*ch)) {\r
++              errno =3D 0;\r
++              *valp =3D strtol(ch, (char**)&ch, 10);\r
++\r
++              loc->end =3D ch;\r
++              *inputp =3D ch;\r
++\r
++              if (errno) {\r
++                      parse_date_print_error(loc, "number out of range");\r
++                      return 256;\r
++              }\r
++              return ch - loc->start =3D=3D 4 ? T_NUMBER_4 : T_NUMBER;\r
++      }\r
++\r
++      if (!isalpha(*ch)) {\r
++              *inputp =3D ch + 1;\r
++              loc->end =3D ch + 1;\r
++              return *ch;\r
++      }\r
++\r
++      /* So it's a string token. */\r
++      str =3D ch;\r
++      while (isalpha(*ch)) {\r
++              ++ch;\r
++      }\r
++      loc->end =3D ch;\r
++      *inputp =3D ch;\r
++\r
++      tk =3D find_token(tokens_array, str, ch - str);\r
++      if (tk) {\r
++              *valp =3D tk->num;\r
++              return tk->token;\r
++      }\r
++\r
++      /* Let's try with locale strings. */\r
++      if (!locale_tokens_populated) {\r
++              populate_locale_tokens();\r
++              locale_tokens_populated =3D true;\r
++      }\r
++      tk =3D find_token(locale_tokens_array, str, ch - str);\r
++      if (tk) {\r
++              *valp =3D tk->num;\r
++              return tk->token;\r
++      }\r
++\r
++      /* If it's just one letter, return it converted to lower case */\r
++      if (ch - str =3D=3D 1) {\r
++              return tolower(*str);\r
++      }\r
++\r
++      parse_date_print_error(loc, "unrecognised token");\r
++      return 256;\r
++}\r
++\r
++\r
++/**************************** Parsing interface **************************=\r
+***/\r
++\r
++static bool parse_date(struct date *ret, const char *from, char *to) {\r
++      char tmp;\r
++      int res;\r
++      if (to) {\r
++              tmp =3D *to;\r
++              *to =3D '\0';\r
++      }\r
++      res =3D yyparse(ret, &from);\r
++      if (to) {\r
++              *to =3D tmp;\r
++      }\r
++      return res =3D=3D 0;\r
++}\r
++\r
++bool parse_range(char *arg, time_t *from, time_t *to) {\r
++      bool left =3D false, right =3D false;\r
++      struct date a, b;\r
++      char *dd, *pp;\r
++\r
++      SKIP_WHITE_SPACE(arg);\r
++      if (!*arg) {\r
++              fprintf(stderr, "empty range argument\n");\r
++              return false;\r
++      }\r
++\r
++      dd =3D strstr(arg, "..");\r
++      pp =3D strstr(arg, "++");\r
++      if ((dd && pp) ||\r
++          (dd && strstr(dd + 2, "..")) ||\r
++          (pp && strstr(pp + 2, "++"))) {\r
++              fprintf(stderr,\r
++                      "%s: at most one of '..' or '++' can be used\n", arg);\r
++              return false;\r
++      }\r
++\r
++      if (dd || pp) {\r
++              char *ch =3D dd ? dd : pp;\r
++              left =3D ch !=3D arg;\r
++              SKIP_WHITE_SPACE(ch);\r
++              right =3D *ch;\r
++      }\r
++      if (pp && (!right || !left)) {\r
++              fprintf(stderr,\r
++                      "%s: '++' requires expression on both sides\n", arg);\r
++              return false;\r
++      }\r
++\r
++      if (left || !right) {  /* date:<date>.. or date:<date> */\r
++              date_set_to_now(&a);\r
++              if (!parse_date(&a, arg, dd ? dd : pp)) {\r
++                      return false;\r
++              }\r
++      }\r
++      if (right) {  /* date:<date>..<date> or date:..<date> */\r
++              if (pp) {\r
++                      b =3D a;\r
++              } else {\r
++                      date_set_to_now(&b);\r
++              }\r
++              if (!parse_date(&b, (dd ? dd : pp) + 2, NULL)) {\r
++                      return false;\r
++              }\r
++      } else if (!left) {  /* date:date */\r
++              left =3D right =3D true;  /* convert to date:<date>..<date> */\r
++              b =3D a;\r
++              if ((b.dur_sec && !date_add_seconds(&b, b.dur_sec, 1)) ||\r
++                  (b.dur_mon && !date_add_months(&b, b.dur_mon, 1))) {\r
++                      right =3D false;  /* convert to date:<date>.. */\r
++              }\r
++      }\r
++\r
++      *from =3D left ? mktime(&a.tm) : 0;\r
++      *to =3D right ? mktime(&b.tm) : (time_t)((unsigned long)~(time_t)0 >> 1);\r
++      return true;\r
++}\r
+diff --git a/date-parser.h b/date-parser.h\r
+new file mode 100644\r
+index 0000000..fb0f19b\r
+--- /dev/null\r
++++ b/date-parser.h\r
+@@ -0,0 +1,59 @@\r
++/* Date parser header file.\r
++ * Copyright (c) 2012 Google Inc.\r
++ * Written by Michal Nazarewicz <mina86@mina86.com>\r
++ *\r
++ * This program is free software: you can redistribute it and/or modify\r
++ * it under the terms of the GNU General Public License as published by\r
++ * the Free Software Foundation, either version 3 of the License, or\r
++ * (at your option) any later version.\r
++ *\r
++ * This program is distributed in the hope that it will be useful,\r
++ * but WITHOUT ANY WARRANTY; without even the implied warranty of\r
++ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\r
++ * GNU General Public License for more details.\r
++ *\r
++ * You should have received a copy of the GNU General Public License\r
++ * along with this program.  If not, see http://www.gnu.org/licenses/ . */\r
++\r
++#ifndef H_DATE_PARSER_H\r
++#define H_DATE_PARSER_H\r
++\r
++#include <stdbool.h>\r
++#include <stdio.h>\r
++#include <time.h>\r
++\r
++bool parse_range(char *arg, time_t *from, time_t *to);\r
++\r
++/* For parser */\r
++struct date;\r
++\r
++struct yylocation {\r
++      const char *start, *end;\r
++};\r
++\r
++static inline void parse_date_print_error(struct yylocation *loc,\r
++                                        const char *message) {\r
++      fprintf(stderr, "%.*s: %s\n",\r
++              (int)(loc->end - loc->start), loc->start, message);\r
++}\r
++\r
++static inline int yyerror(struct yylocation *loc, struct date *ret,\r
++                        const char **inputp, const char *message) {\r
++      ret =3D ret; /* make compiler happy */\r
++      inputp =3D inputp;\r
++      parse_date_print_error(loc, message);\r
++      return 0;\r
++}\r
++\r
++int yylex(long *valp, struct yylocation *loc, const char **inputp);\r
++int yyparse(struct date *ret, const char **inputp);\r
++\r
++void date_set_from_stamp(struct date *ret, long stamp);\r
++bool date_add_seconds(struct date *ret, long num, long unit);\r
++bool date_set_yesterday(struct date *ret);\r
++bool date_add_months(struct date *ret, long num, long unit);\r
++bool date_set_time(struct date *ret, long h, long m, long s, int ampm);\r
++bool date_set_date(struct date *ret, long y, long m, long d);\r
++bool date_set_quarter(struct date *ret, long y, long q);\r
++\r
++#endif\r
+diff --git a/test.c b/test.c\r
+new file mode 100644\r
+index 0000000..c4e2d9c\r
+--- /dev/null\r
++++ b/test.c\r
+@@ -0,0 +1,44 @@\r
++/* Date parser testing application.\r
++ * Copyright (c) 2012 Google Inc.\r
++ * Written by Michal Nazarewicz <mina86@mina86.com>\r
++ *\r
++ * This program is free software: you can redistribute it and/or modify\r
++ * it under the terms of the GNU General Public License as published by\r
++ * the Free Software Foundation, either version 3 of the License, or\r
++ * (at your option) any later version.\r
++ *\r
++ * This program is distributed in the hope that it will be useful,\r
++ * but WITHOUT ANY WARRANTY; without even the implied warranty of\r
++ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\r
++ * GNU General Public License for more details.\r
++ *\r
++ * You should have received a copy of the GNU General Public License\r
++ * along with this program.  If not, see http://www.gnu.org/licenses/ . */\r
++\r
++#define _POSIX_C_SOURCE 1\r
++\r
++#include <locale.h>\r
++#include <stdio.h>\r
++#include <time.h>\r
++\r
++#include "date-parser.h"\r
++\r
++int main(void) {\r
++      char buf[1024], *ch;\r
++      time_t from, to;\r
++      struct tm tm;\r
++\r
++      setlocale(LC_ALL, "");\r
++\r
++      while (fgets(buf, sizeof buf, stdin)) {\r
++              if (parse_range(buf, &from, &to)) {\r
++                      localtime_r(&from, &tm);\r
++                      ch =3D buf + strftime(buf, sizeof buf / 2,\r
++                                          "[%Y/%m/%d %H:%M:%S %Z, ", &tm);\r
++                      localtime_r(&to, &tm);\r
++                      strftime(ch, sizeof buf / 2,\r
++                               "%Y/%m/%d %H:%M:%S %Z)\n", &tm);\r
++                      fputs(buf, stdout);\r
++              }\r
++      }\r
++}\r
+--=20\r
+1.7.7.3\r
+\r
+--=20\r
+Best regards,                                         _     _\r
+.o. | Liege of Serenely Enlightened Majesty of      o' \,=3D./ `o\r
+..o | Computer Science,  Micha=C5=82 =E2=80=9Cmina86=E2=80=9D Nazarewicz   =\r
+ (o o)\r
+ooo +----<email/xmpp: mpn@google.com>--------------ooO--(_)--Ooo--\r
+--=-=-=\r
+Content-Type: multipart/signed; boundary="==-=-=";\r
+       micalg=pgp-sha1; protocol="application/pgp-signature"\r
+\r
+--==-=-=\r
+Content-Type: text/plain\r
+\r
+\r
+--==-=-=\r
+Content-Type: application/pgp-signature\r
+\r
+-----BEGIN PGP SIGNATURE-----\r
+Version: GnuPG v1.4.10 (GNU/Linux)\r
+\r
+iQIcBAEBAgAGBQJQVzAPAAoJECBgQBJQdR/0e8UP/jA6s6ANgAHVEcfvd4nvCpaZ\r
+DEBV1hpm6HGJDQ/P1NPRafn6k4ZdaZokCK1qWMjgmYNOQ6jmfNLhieGuEXXaTsQs\r
+NRmroRQfWpDAIeQgsF/9dIAZgFoCigLKt/ge0p4e/rHBePmYOfxr3T9RBuGw9drv\r
+/zZ+fpv55P6iZP+xzl8TxeeVL9CDbzkuytnH3Kg1nFKgEFHlRoZxjeq/iELKXz/n\r
+reMF3cxoAZkYXwW5WntA/mWUqUFptWgBYZSi6bPhg6KEbJdr2lu6L3kxrYbuSUn9\r
+MBRIqG60OWVwguoCUbHE71mv39tnINb7uHTZumvdQkBlr2deeXPzho1xCwqMQH17\r
+sZ6IkSe2hhfaCxgBWhddzvHHr7TPIuAVZGHWucJuWOHCvBasn21ARtcOn/q0dhVT\r
+XGTvAw072lbo2l/ceaEQTGDrj1IV8Xo/1OeQU4y6n56hrVDknb4zwkdlk1BIcNu/\r
+Upi2vgewDETFl+NuLE7rr2/4UNcXAgBQIZnwlL5+vo/EV2/eTsTY0TemYxq/ppZC\r
+/YtV1x35sc1pN5LswSGU6tDeFp0+TNXaPXEW48IPrE5OhmaCrQT84DK62+UJDJ8r\r
+Ga4MmQh6GFQ3EemKoDHim59gp2hC3HOrHiQ9MWlf2GG0zYVOKto02qjPCjoeRjzc\r
+ZTefvm76BBIEpTfwa1fx\r
+=STYS\r
+-----END PGP SIGNATURE-----\r
+--==-=-=--\r
+\r
+--=-=-=--\r