branch: elpa/datetime
commit 6982c383aa5180eee004a0d78cd06cb56ef34c3d
Author: Paul Pogonyshev <pogonys...@gmail.com>
Commit: Paul Pogonyshev <pogonys...@gmail.com>

    Implement timestamp parsing (incomplete, but works for most typical cases).
---
 datetime.el                | 502 +++++++++++++++++++++++++++++++++++++++++++--
 run-tests.sh               |   7 +-
 test/FormatTimestamp.java  |  36 ----
 test/ProcessTimestamp.java | 108 ++++++++++
 test/format.el             | 129 +++++-------
 test/parse.el              | 141 +++++++++++++
 test/test.el               |  70 +++++++
 7 files changed, 853 insertions(+), 140 deletions(-)

diff --git a/datetime.el b/datetime.el
index 062d65df66..4c6ee71b20 100644
--- a/datetime.el
+++ b/datetime.el
@@ -115,10 +115,14 @@
 (require 'extmap)
 
 
-(if (fboundp 'define-error)
-    (define-error 'datetime-unsupported-timezone "Timezones are currently not 
supported")
-  (put 'datetime-unsupported-timezone 'error-conditions 
'(datetime-unsupported-timezone error))
-  (put 'datetime-unsupported-timezone 'error-message "Timezones are currently 
not supported"))
+(defun datetime--define-error (name message)
+  (if (fboundp #'define-error)
+      (define-error name message)
+    (put name 'error-conditions `(,name error))
+    (put name 'error-message    message)))
+
+(datetime--define-error 'datetime-invalid-string       "Date-time string is 
invalid")
+(datetime--define-error 'datetime-unsupported-timezone "Timezones are 
currently not supported")
 
 
 (defconst datetime--directory (file-name-directory (or load-file-name 
(buffer-file-name))))
@@ -398,36 +402,42 @@ when necessary."
     (apply #'concat (nreverse strings))))
 
 
-(defsubst datetime--gregorian-leap-year-p (year)
+(defsubst datetime--gregorian-leap-year-mod-400-p (year-mod-400)
   (aref (eval-when-compile (let (result)
                              (dotimes (year 400)
                                (push (and (= (% year 4) 0) (or (/= (% year 
100) 0) (= (% year 400) 0))) result))
                              (with-no-warnings (apply (if (fboundp 
#'bool-vector) #'bool-vector #'vector) (nreverse result)))))
-        (mod year 400)))
+        year-mod-400))
+
+(defsubst datetime--gregorian-leap-year-p (year)
+  (datetime--gregorian-leap-year-mod-400-p (mod year 400)))
 
 (defconst datetime--gregorian-cumulative-year-days (let ((days 0)
                                                          result)
                                                      (dotimes (year 400)
                                                        (push days result)
-                                                       (setq days (+ days (if 
(datetime--gregorian-leap-year-p year) 366 365))))
+                                                       (setq days (+ days (if 
(datetime--gregorian-leap-year-mod-400-p year) 366 365))))
                                                      (push days result)
                                                      (apply #'vector (nreverse 
result))))
-(defconst datetime--gregorian-days-in-400-years (aref 
datetime--gregorian-cumulative-year-days 400))
+(defconst datetime--gregorian-days-in-400-years    (aref 
datetime--gregorian-cumulative-year-days 400))
+(defconst datetime--gregorian-days-in-1970-years   (+ (* 
datetime--gregorian-days-in-400-years (/ 1970 400))
+                                                      (aref 
datetime--gregorian-cumulative-year-days (% 1970 400))))
 
 ;; Conveniently, this also has a loop size of 400 years.
 (defconst datetime--gregorian-first-day-of-year (let ((first-day 5)
                                                       result)
                                                   (dotimes (year 400)
                                                     (push first-day result)
-                                                    (setq first-day (% (+ 
first-day (if (datetime--gregorian-leap-year-p year) 2 1)) 7)))
+                                                    (setq first-day (% (+ 
first-day (if (datetime--gregorian-leap-year-mod-400-p year) 2 1)) 7)))
                                                   (apply #'vector (nreverse 
result))))
 
 (defconst datetime--average-seconds-in-year (/ (* 
datetime--gregorian-days-in-400-years 24 60 60) 400))
 
 ;; For non-leap years.
+(defconst datetime--gregorian-month-days            [31 28 31 30 31 30 31 31 
30 31 30 31])
 (defconst datetime--gregorian-cumulative-month-days (let ((days   0)
                                                           (result (list 0)))
-                                                      (dolist (month-days '(31 
28 31 30 31 30 31 31 30 31 30 31))
+                                                      (dolist (month-days 
(append datetime--gregorian-month-days nil))
                                                         (push (setq days (+ 
days month-days)) result))
                                                       (apply #'vector 
(nreverse result))))
 
@@ -479,10 +489,8 @@ to this function.
              (setq need-year t)
              (push "%s" format-parts)
              (push `(aref ,(datetime-locale-field locale :eras) (if (> year 0) 
1 0)) format-arguments))
-            ((or `year `year-for-week)
+            (`year
              (setq need-year t)
-             (when (eq type 'year-for-week)
-               (setq need-day t))
              (push (pcase details
                      (`add-century-when-parsing "%d")
                      (`always-two-digits        "%02d")
@@ -494,6 +502,8 @@ to this function.
                    format-arguments)
              (when (eq details 'always-two-digits)
                (setcar format-arguments `(mod ,(car format-arguments) 100))))
+            (`year-for-week
+             (error "Formatting `%s' is currently not implemented" type))
             (`month
              (setq need-month t)
              (push (datetime--digits-format details) format-parts)
@@ -575,19 +585,21 @@ to this function.
                                              `(datetime--convert-to-utc-float 
(float date-time) ,(datetime--macroexp-quote timezone-data)))))
                          (let* (,@(when (or need-year need-month need-weekday 
need-day)
                                     ;; Date in days, rebased from 1970-01-01 
to 0000-01-01.
-                                    `((date-0           (+ (floor (/ date-time 
,(* 24 60 60)))
-                                                           ,(+ (* 
days-in-400-years (/ 1970 400)) (aref datetime--gregorian-cumulative-year-days 
(% 1970 400)))))
+                                    `((date-0           (+ (floor (/ date-time 
,(* 24 60 60))) ,datetime--gregorian-days-in-1970-years))
                                       (date-%-400-years (mod date-0 
,days-in-400-years))
                                       (full-400-years   (/ (- date-0 
date-%-400-years) ,days-in-400-years))
+                                      (year-%-400       (/ date-%-400-years 
366))
                                       (year             (+ (* full-400-years 
400)
-                                                           (let ((year-%-400 
(/ date-%-400-years 366)))
+                                                           (progn
                                                              (if (< 
date-%-400-years (aref ,datetime--gregorian-cumulative-year-days (1+ 
year-%-400)))
                                                                  year-%-400
-                                                               (1+ 
year-%-400)))))))
+                                                               (setq 
year-%-400 (1+ year-%-400))))))))
                                 ,@(when (or need-month need-weekday need-day)
                                     `((year-day         (- date-0 (* 
full-400-years ,days-in-400-years) (aref 
,datetime--gregorian-cumulative-year-days (mod year 400))))
                                       (day              year-day)
-                                      (month            (let ((july-days (if 
(datetime--gregorian-leap-year-p year) ,(+ 31 29 31 30 31 30) ,(+ 31 28 31 30 
31 30))))
+                                      (month            (let ((july-days (if 
(datetime--gregorian-leap-year-mod-400-p year-%-400)
+                                                                             
,(+ 31 29 31 30 31 30)
+                                                                           ,(+ 
31 28 31 30 31 30))))
                                                           (if (>= day 
july-days)
                                                               (if (>= (setq 
day (- day july-days)) ,(+ 31 31 30))
                                                                   (cond ((< 
(setq day (- day ,(+ 31 31 30))) 31)  9)           ; October
@@ -691,6 +703,460 @@ to this function.
     (aset all-year-transitions year-offset (nreverse transitions))))
 
 
+;; There is horribly unreadable level of backquoting/unquoting inside this 
macro...
+(defmacro datetime--parser-computation (pattern value-name validating min max 
&rest arguments)
+  (let ((computations    (make-symbol "$computations"))
+        (computation     (make-symbol "$computation"))
+        (range-validated (make-symbol "$range-validated"))
+        loops)
+    (setq arguments (reverse arguments))
+    (while arguments
+      (let* ((set             (pop arguments))
+             (part-indices    (nth 0 set))
+             (builder         (nth 1 set))
+             (self-validating (nth 2 set))
+             (new-loop        `(while ,part-indices
+                                 (push (,(if (consp builder) (car builder) 
builder) (pop ,part-indices) ,@(when (consp builder) (cdr builder)))
+                                       ,computations))))
+        (when (and self-validating (or min max))
+          (setq new-loop `(progn (when ,part-indices (setq ,range-validated 
t)) ,new-loop)))
+        (setq loops
+              (if loops
+                  `(,@(macroexp-unprogn new-loop)
+                    (when (or ,validating (null ,computations))
+                      ,@loops))
+                `(,new-loop)))))
+    `(let (,computations
+           ,@(when (or min max) `(,range-validated)))
+       ,@loops
+       (when ,computations
+         (let ((,computation (if (cdr ,computations)
+                                 `(let ((x ,(car ,computations)))
+                                    ,@(mapcar (lambda (computation)
+                                                `(unless (eq ,computation x)
+                                                   (signal 
'datetime-invalid-string (list string ,,pattern ,,(format "inconsistent %s" 
value-name)))))
+                                              (cdr ,computations))
+                                    x)
+                               (car ,computations))))
+           ,@(when (or min max)
+               `((when (and ,validating (not ,range-validated))
+                   (setq ,computation `(let ((x ,,computation))
+                                         (unless ,',(cond ((and min max) `(<= 
,min x ,max))
+                                                          (min           `(<= 
,min x))
+                                                          (t             `(<= 
x ,max)))
+                                           (signal 'datetime-invalid-string 
(list string ,,pattern ,,(format "%s is out of range" value-name))))
+                                         x)))))
+           ,computation)))))
+
+(defun datetime-parser-to-float (type pattern &rest options)
+  "Return a function that parses date-time according to the PATTERN.
+Argument TYPE defines how the pattern should be interpreted, see
+library documentation.  Rest of the arguments must be a property
+list, i.e. keywords interleaved with values.
+
+The resulting function transforms a string to a float number of
+seconds since the epoch (0:00:00 of 1st of January 1970), in UTC
+timezone.  The function is byte-compiled, unless you specify
+:debug option.  Behavior for invalid strings depends on whether
+:non-validating option is specified.
+
+OPTIONS should be any keyword arguments understood by
+`datetime-recode-pattern' plus any from the list below, specific
+to this function.  Default value of keyword arguments is nil
+unless specified otherwise.
+
+  :locale
+
+    Locale (language) used for month, weekday etc. names.  Always
+    defaults to English, even if system locale is different.
+
+  :timezone
+
+    The timezone for parsing input strings in.  Always defaults
+    to UTC.  You can use special value \\='system to let the
+    library find the value suitable for the current machine.
+
+    If input string explicitly specifies a timezone (i.e. if
+    PATTERN does), this value is essentially ignored.
+
+  :defaults
+
+    A plist of values for those date/time part that are not
+    specified in the input.  Accepted keys:
+
+      year   -- defaults to 1970 (the year of UNIX epoch);
+      month  -- must be in the range 1 to 12, defaults to 1;
+      day    -- must be in the range 1 to 31, defaults to 1; will
+                cause validation errors if used and is too large
+                for the parsed month and year;
+      hour   -- must be in the range 0 to 23, defaults to 0;
+      minute -- must be in the range 0 to 59, defaults to 0;
+      second -- must be in the range 0 to 59, defaults to 0.
+
+    Note that the set of accepted keys is substantially smaller
+    than that of all understood pattern parts.  For example, eras
+    are not supported (use negative years), or 12-hour clock time
+    (convert to 24-hour).
+
+    If PATTERN specifies a way certain value is encoded in input
+    strings, corresponding value from this plist is ignored.
+
+  :non-validating
+
+    Validating parsers always signal a `datetime-invalid-string'
+    error if given strings that cannot be parsed or contain
+    invalid values like 30th of February.  Non-validating parsers
+    can either return unspecified numeric result or signal
+    arbitrary errors in such cases.  (But it is guaranteed they
+    don't fall into an infinite loop or perform any other
+    action.)
+
+    Non-validating parsers are more efficient, for some patterns
+    considerably so.
+
+  :case-insensitive
+
+    Accept text in any case.  This works both for literal text
+    included in the pattern and for month etc. names.
+
+  :lax-whitespace
+
+    Match any whitespace in PATTERN against any whitespace in
+    date-time string.  For this purpose \"whitespace\" is defined
+    as space and tab characters only.
+
+  :accept-leading-space
+
+    Make variable-width numbers (e.g. day number without leading
+    zero) match also if there is a leading space.
+
+  :debug
+
+    Don't byte-compile the parser function, leave it in the form
+    of Lisp lambda."
+  (let* ((locale           (datetime--get-locale options))
+         (timezone         (datetime--get-timezone options))
+         (timezone-data    (or (extmap-get datetime--timezone-extmap timezone 
t)
+                               (error "Unknown timezone `%s'" timezone)))
+         (defaults         (plist-get options :defaults))
+         (validating       (not (plist-get options :non-validating)))
+         (case-insensitive (and (plist-get options :case-insensitive) t))
+         (lax-whitespace   (plist-get options :lax-whitespace))
+         (part-index       0)
+         regexp-parts
+         ;; To handle excessive information patterns (e.g. "Mon 16 Sep 2018" 
is excessive,
+         ;; since day of the week can be found from the day of the year), we 
keep track of
+         ;; all the various groups and decide which to use later.  Groups are 
also stored
+         ;; as a list (or alist in certain cases), though this is hardly 
necessary, since
+         ;; normally patterns wouldn't repeat the same group.
+         era-part-indices
+         year-part-indices
+         month-number-part-indices
+         month-name-part-indices
+         day-of-month-part-indices
+         am-pm-part-indices
+         hour-0-23-part-indices
+         hour-1-24-part-indices
+         hour-am-pm-1-12-part-indices
+         hour-am-pm-0-11-part-indices
+         minute-part-indices
+         second-part-indices
+         second-fractional-part-indices
+         have-case-sensitive-parts)
+    (dolist (part (datetime--parse-pattern type pattern options))
+      (if (stringp part)
+          (let ((quoted (regexp-quote part)))
+            (when (not (or have-case-sensitive-parts (string= (upcase part) 
(downcase part))))
+              (setq have-case-sensitive-parts t))
+            (push (if lax-whitespace
+                      (replace-regexp-in-string (rx (1+ (any blank))) (rx (1+ 
(any blank))) quoted t t)
+                    quoted)
+                  regexp-parts))
+        (let* ((type    (car part))
+               (details (cdr part))
+               (regexp  (pcase type
+                          (`era                   (when (or validating (null 
era-part-indices))
+                                                    (push part-index 
era-part-indices))
+                                                  (datetime-locale-field 
locale :eras))
+                          (`year
+                           (when (or validating (null year-part-indices))
+                             (push (cons part-index details) 
year-part-indices))
+                           (cond ((or (memq details '(1 
add-century-when-parsing)) (not (plist-get options :require-leading-zeros)))
+                                  (rx (1+ (any "0-9"))))
+                                 ((memq details '(2 always-two-digits))
+                                  (rx (any "0-9") (1+ (any "0-9"))))
+                                 (t
+                                  (format "[0-9]\\{%d\\}[0-9]+" (1- 
details)))))
+                          (`year-for-week         (error "Parsing `%s' is 
currently not implemented" type))
+                          (`month                 (when (or validating (null 
month-number-part-indices))
+                                                    (push part-index 
month-number-part-indices))
+                                                  12)
+                          (`month-context-name    (let ((field (if (eq details 
'abbreviated) :month-context-abbr :month-context-names)))
+                                                    (when (or validating (null 
month-name-part-indices))
+                                                      (push (cons part-index 
field) month-name-part-indices))
+                                                    (datetime-locale-field 
locale field)))
+                          (`month-standalone-name (let ((field (if (eq details 
'abbreviated) :month-standalone-abbr :month-standalone-names)))
+                                                    (when (or validating (null 
month-name-part-indices))
+                                                      (push (cons part-index 
field) month-name-part-indices))
+                                                    (datetime-locale-field 
locale field)))
+                          (`week-in-year         (error "Parsing `%s' is 
currently not implemented" type))
+                          (`week-in-month        (error "Parsing `%s' is 
currently not implemented" type))
+                          (`day-in-month         (when (or validating (null 
day-of-month-part-indices))
+                                                   (push part-index 
day-of-month-part-indices))
+                                                 31)
+                          (`weekday-in-month     (error "Parsing `%s' is 
currently not implemented" type))
+                          (`weekday               7)
+                          (`weekday-context-name
+                           (datetime-locale-field locale (if (eq details 
'abbreviated) :weekday-context-abbr    :weekday-context-names)))
+                          (`weekday-standalone-name
+                           (datetime-locale-field locale (if (eq details 
'abbreviated) :weekday-standalone-abbr :weekday-standalone-names)))
+                          (`am-pm                (when (or validating (null 
am-pm-part-indices))
+                                                   (push part-index 
am-pm-part-indices))
+                                                 (datetime-locale-field locale 
:am-pm))
+                          (`hour-0-23            (when (or validating (null 
hour-0-23-part-indices))
+                                                   (push part-index 
hour-0-23-part-indices))
+                                                 23)
+                          (`hour-1-24            (when (or validating (null 
hour-1-24-part-indices))
+                                                   (push part-index 
hour-1-24-part-indices))
+                                                 24)
+                          (`hour-am-pm-0-11      (when (or validating (null 
hour-am-pm-0-11-part-indices))
+                                                   (push part-index 
hour-am-pm-0-11-part-indices))
+                                                 11)
+                          (`hour-am-pm-1-12      (when (or validating (null 
hour-am-pm-1-12-part-indices))
+                                                   (push part-index 
hour-am-pm-1-12-part-indices))
+                                                 12)
+                          (`minute               (when (or validating (null 
minute-part-indices))
+                                                   (push part-index 
minute-part-indices))
+                                                 59)
+                          (`second               (when (or validating (null 
second-part-indices))
+                                                   (push part-index 
second-part-indices))
+                                                  59)
+                          (`decimal-separator    (rx (or "." ",")))
+                          (`millisecond          (push (cons part-index 
1000.0) second-fractional-part-indices)
+                                                 (rx (any "0-9") (any "0-9") 
(any "0-9")))
+                          (`second-fractional    (push (cons part-index (expt 
10.0 details)) second-fractional-part-indices)
+                                                 (apply #'concat (make-list 
details (rx (any "0-9")))))
+                          (`timezone
+                           (signal 'datetime-unsupported-timezone nil))
+                          (_ (error "Unexpected value %s" type)))))
+          (push (cond ((integerp regexp)
+                       ;; REGEXP is really the maximum value of this one- or 
two-digit
+                       ;; number.  However, we don't include it in the regexp 
in most of
+                       ;; the cases (unlike in `datetime-matching-regexp').
+                       (if (<= regexp 9)
+                           (format "0*[1-%d]" regexp)
+                         (cond ((and (= details 1) (plist-get options 
:accept-leading-space))
+                                (format "[ 0-%d]?[0-9]" (/ regexp 10)))
+                               ((>= regexp 20)
+                                (format "0*[1-%d]?[0-9]" (/ regexp 10)))
+                               (t
+                                "0*1?[0-9]"))))
+                      ((vectorp regexp)
+                       ;; A vector of options returned by 
`datetime-locale-field'.
+                       (setq have-case-sensitive-parts t)
+                       (regexp-opt (append regexp nil)))
+                      (t
+                       regexp))
+                regexp-parts)))
+      (setq part-index (1+ part-index)))
+    (setq era-part-indices               (nreverse era-part-indices)
+          year-part-indices              (nreverse year-part-indices)
+          month-number-part-indices      (nreverse month-number-part-indices)
+          month-name-part-indices        (nreverse month-name-part-indices)
+          day-of-month-part-indices      (nreverse day-of-month-part-indices)
+          am-pm-part-indices             (nreverse am-pm-part-indices)
+          hour-0-23-part-indices         (nreverse hour-0-23-part-indices)
+          hour-1-24-part-indices         (nreverse hour-1-24-part-indices)
+          hour-am-pm-1-12-part-indices   (nreverse 
hour-am-pm-1-12-part-indices)
+          hour-am-pm-0-11-part-indices   (nreverse 
hour-am-pm-0-11-part-indices)
+          regexp-parts                   (nreverse regexp-parts)
+          minute-part-indices            (nreverse minute-part-indices)
+          second-part-indices            (nreverse second-part-indices)
+          second-fractional-part-indices (nreverse 
second-fractional-part-indices))
+    (unless validating
+      (when month-number-part-indices
+        (setq month-name-part-indices nil))
+      (cond (hour-0-23-part-indices
+             (setq hour-1-24-part-indices       nil
+                   hour-am-pm-1-12-part-indices nil
+                   hour-am-pm-0-11-part-indices nil))
+            (hour-1-24-part-indices
+             (setq hour-am-pm-1-12-part-indices nil
+                   hour-am-pm-0-11-part-indices nil))
+            (hour-am-pm-0-11-part-indices
+             (setq hour-am-pm-1-12-part-indices nil))))
+    (let* ((regexp-parts            regexp-parts)
+           (substituting-indices-in (list era-part-indices
+                                          year-part-indices 
month-number-part-indices month-name-part-indices day-of-month-part-indices
+                                          am-pm-part-indices
+                                          hour-0-23-part-indices 
hour-1-24-part-indices hour-am-pm-1-12-part-indices hour-am-pm-0-11-part-indices
+                                          minute-part-indices 
second-part-indices second-fractional-part-indices))
+           (part-index              0)
+           (group-index             1))
+      (while regexp-parts
+        (let ((substituting-indices-in-scan substituting-indices-in))
+          (while substituting-indices-in-scan
+            (let ((listed-element (car substituting-indices-in-scan)))
+              (when listed-element
+                ;; To handle alists.
+                (unless (numberp (car listed-element))
+                  (setq listed-element (car listed-element)))
+                (when (eq part-index (car listed-element))
+                  (setf (car listed-element)               group-index
+                        (car substituting-indices-in-scan) (cdar 
substituting-indices-in-scan)
+                        (car regexp-parts)                 (concat "\\(" (car 
regexp-parts) "\\)")
+                        group-index                        (1+ group-index)
+                        substituting-indices-in-scan       nil))))
+            (setq substituting-indices-in-scan (cdr 
substituting-indices-in-scan))))
+        (setq regexp-parts (cdr regexp-parts)
+              part-index   (1+ part-index))))
+    (let* ((downcased          (and have-case-sensitive-parts 
case-insensitive))
+           (year-computation   (datetime--parser-computation pattern "year" 
validating nil nil
+                                                             
(year-part-indices datetime--parser-year-computation)))
+           (constant-year      (unless year-computation
+                                 (or (plist-get 'year defaults) 1970)))
+           (era-correction     (when (and year-computation era-part-indices)
+                                 (datetime--parser-computation pattern "era" 
validating nil nil
+                                                               
(era-part-indices (datetime--parser-era-correction locale :eras downcased) t))))
+           (month-computation  (or (datetime--parser-computation pattern 
"month" validating 0 11
+                                                                 
(month-number-part-indices (datetime--parser-int-computation t))
+                                                                 
(month-name-part-indices (datetime--parser-string-index-computation locale nil 
downcased) t))
+                                   (let ((default (plist-get 'month defaults)))
+                                     (if default (1- default) 0))))
+           (day-computation    (or (datetime--parser-computation pattern "day 
of month" validating 0 nil
+                                                                 
(day-of-month-part-indices (datetime--parser-int-computation t)))
+                                   (let ((default (plist-get 'day defaults)))
+                                     (if default (1- default) 0))))
+           (am-pm-computation  (or (datetime--parser-computation pattern 
"am-pm" validating nil nil (am-pm-part-indices 
(datetime--parser-string-if-computation locale :am-pm downcased 0 12) t))
+                                   (plist-get 'am-pm 0)
+                                   0))
+           (hour-computation   (or (datetime--parser-computation pattern 
"hour" validating nil 23
+                                                                 
(hour-0-23-part-indices datetime--parser-int-computation)
+                                                                 
(hour-1-24-part-indices datetime--parser-hour-1-24-computation)
+                                                                 
(hour-am-pm-1-12-part-indices (datetime--parser-hour-am-pm-computation 
am-pm-computation t))
+                                                                 
(hour-am-pm-0-11-part-indices (datetime--parser-hour-am-pm-computation 
am-pm-computation nil)))
+                                   (plist-get 'hour defaults) 0))
+           (minute-computation (or (datetime--parser-computation pattern 
"minute" validating nil 59
+                                                                 
(minute-part-indices datetime--parser-int-computation))
+                                   (plist-get 'minute defaults) 0))
+           (second-computation (or (datetime--parser-computation pattern 
"second" validating nil 59
+                                                                 
(second-part-indices datetime--parser-int-computation))
+                                   (plist-get 'second defaults) 0))
+           (parser            `(+ ,@(when (or year-computation
+                                              (not (memq constant-year     
'(nil 1970)))
+                                              (not (memq month-computation 
'(nil 0)))
+                                              (not (memq day-computation   
'(nil 0))))
+                                      ;; FIXME: Optimize for constant year.
+                                      `((* (let ((year  ,(or year-computation 
constant-year))
+                                                 (month ,month-computation))
+                                             ,@(when era-correction
+                                                 `(,era-correction))
+                                             (let ((year-mod-400 (mod year 
400)))
+                                               (+ (* (/ (- year year-mod-400) 
400) ,datetime--gregorian-days-in-400-years)
+                                                  (aref 
,datetime--gregorian-cumulative-year-days year-mod-400)
+                                                  ,(- 
datetime--gregorian-days-in-1970-years)
+                                                  (aref 
,datetime--gregorian-cumulative-month-days month)
+                                                  (if (and (>= month 2) 
(datetime--gregorian-leap-year-mod-400-p year-mod-400)) 1 0)
+                                                  ,(if validating
+                                                       `(let ((day 
,day-computation))
+                                                          (unless (and (<= 0 
day)
+                                                                       (or (< 
day (aref ,datetime--gregorian-month-days month))
+                                                                           
(and (= month 1) (= day 28)
+                                                                               
 (datetime--gregorian-leap-year-mod-400-p year-mod-400))))
+                                                            (signal 
'datetime-invalid-string (list string ,pattern "day is out of range")))
+                                                          day)
+                                                     day-computation))))
+                                           ,(* 24 60 60))))
+                                  (* ,hour-computation ,(* 60 60))
+                                  (* ,minute-computation 60)
+                                  ,second-computation
+                                  ,@(when second-fractional-part-indices
+                                      `((/ (string-to-number (match-string 
,(caar second-fractional-part-indices) string))
+                                           ,(cdar 
second-fractional-part-indices)))))))
+      (pcase timezone-data
+        (`(,constant-offset)
+         (unless (= constant-offset 0)
+           (setq parser `(- ,parser ,constant-offset))))
+        (_
+         (setq parser `(datetime--convert-from-utc-float ,parser 
,(datetime--macroexp-quote timezone-data)))))
+      (setq parser `(save-match-data
+                      (if (string-match ,(concat "^" (apply #'concat 
regexp-parts) "$") ,(if downcased `(downcase string) 'string))
+                          ,parser
+                        (signal 'datetime-invalid-string (list string ,pattern 
"doesn't match the pattern")))))
+      (when have-case-sensitive-parts
+        (setq parser `(let ((case-fold-search ,case-insensitive))
+                        ,parser)))
+      (setq parser `(lambda (string) ,parser))
+      (if (plist-get options :debug)
+          parser
+        (byte-compile parser)))))
+
+(defun datetime--parser-year-computation (argument)
+  (pcase (cdr argument)
+    (`add-century-when-parsing `(let ((year ,(datetime--parser-int-computation 
(car argument))))
+                                  (if (= (length (match-string ,(car argument) 
string)) 2)
+                                      (+ year 2000)
+                                    year)))
+    (`always-two-digits        `(+ ,(datetime--parser-int-computation (car 
argument)) 2000))
+    (_                         (datetime--parser-int-computation (car 
argument)))))
+
+(defun datetime--parser-era-correction (argument locale field downcased)
+  (datetime--parser-string-if-computation argument locale field downcased 
`(setq year (- 1 year)) nil))
+
+(defun datetime--parser-hour-1-24-computation (argument)
+  `(let ((hour-1-24 ,(datetime--parser-int-computation argument)))
+     (if (< hour-1-24 24) hour-1-24 0)))
+
+(defun datetime--parser-hour-am-pm-computation (argument am-pm-computation 
expect-1-12)
+  (if expect-1-12
+      `(let ((hour-am-pm-1-12 ,(datetime--parser-int-computation argument)))
+         (+ (if (< hour-am-pm-1-12 12) hour-am-pm-1-12 0) ,am-pm-computation))
+    `(+ ,(datetime--parser-int-computation argument) ,am-pm-computation)))
+
+(defun datetime--parser-int-computation (argument &optional off-by-one)
+  (let ((computation `(string-to-number (match-string ,(if (consp argument) 
(car argument) argument) string))))
+    (if off-by-one
+        `(1- ,computation)
+      computation)))
+
+(defun datetime--parser-string-index-computation (argument locale field 
downcased)
+  (let ((strings (datetime-locale-field locale (or field (cdr argument)))))
+    (when downcased
+      (setq strings (vconcat (mapcar #'downcase (append strings nil)))))
+    `(let ((match (match-string ,(if (consp argument) (car argument) argument) 
string))
+           (n     0))
+       (while (not (string= match (aref ,strings n)))
+         (setq n (1+ n)))
+       n)))
+
+(defun datetime--parser-string-if-computation (argument locale field downcased 
if-first-form if-second-form)
+  (let ((strings (datetime-locale-field locale field)))
+    (unless (= (length strings) 2)
+      (error "Must be called only for two-string fields"))
+    `(if (string= (match-string ,(if (consp argument) (car argument) argument) 
string)
+                  ,(if downcased (downcase (aref strings 0)) (aref strings 0)))
+         ,if-first-form
+       ,if-second-form)))
+
+;; Pretty similar to `datetime--convert-to-utc-float', but not quite.
+(defun datetime--convert-from-utc-float (date-time timezone-data)
+  (let ((year-offset          (floor (/ (- date-time (car timezone-data)) 
datetime--average-seconds-in-year)))
+        (all-year-transitions (nth 1 timezone-data)))
+    (if (>= year-offset 0)
+        (let* ((year-transitions (or (when (< year-offset (length 
all-year-transitions))
+                                       (aref all-year-transitions year-offset))
+                                     (datetime--calculate-year-transitions 
timezone-data year-offset)))
+               (offset           (pop year-transitions)))
+          (when year-transitions
+            (let ((offset-in-year (floor (- date-time (car timezone-data) (* 
year-offset datetime--average-seconds-in-year)))))
+              (while (and (>= (- offset-in-year offset) (car year-transitions))
+                          (setq offset           (cadr year-transitions)
+                                year-transitions (cddr year-transitions))))))
+          (- date-time offset))
+      ;; Offset before the very first transition.
+      (- date-time (car (aref all-year-transitions 0))))))
+
+
 (defun datetime-matching-regexp (type pattern &rest options)
   "Return a regexp that matches date-time according to the PATTERN.
 Argument TYPE defines how the pattern should be interpreted, see
diff --git a/run-tests.sh b/run-tests.sh
index 5bee85906a..cb0218c353 100755
--- a/run-tests.sh
+++ b/run-tests.sh
@@ -14,6 +14,7 @@
 set -e
 
 OWN_DIRECTORY=$(dirname $0)
+cd $OWN_DIRECTORY
 
 if [ -z "$EMACS" ]; then
     EMACS=emacs
@@ -28,21 +29,21 @@ if [ -z "$ERT_SELECTOR" ]; then
 fi
 
 cd test
-javac FormatTimestamp.java
+javac ProcessTimestamp.java
 cd ..
 
 $EMACS --batch                                                                 
                                              \
        --eval "(message \"Using Emacs %s\" (emacs-version))"                   
                                              \
        --eval "(progn (require 'package) (package-initialize))"                
                                              \
-       --directory "$OWN_DIRECTORY"                                            
                                              \
        --eval "(when (locate-file \"local-environment.el\" (list (car 
load-path))) (load \"local-environment.el\" nil t t))" \
        -l datetime.el                                                          
                                              \
+       -l test/test.el                                                         
                                              \
        -l test/format.el                                                       
                                              \
+       -l test/parse.el                                                        
                                              \
        --eval "(ert-run-tests-batch-and-exit (quote ${ERT_SELECTOR}))"
 
 $EMACS --batch                                                                 
                                              \
        --eval "(progn (require 'package) (package-initialize))"                
                                              \
-       --directory "$OWN_DIRECTORY"                                            
                                              \
        --eval "(when (locate-file \"local-environment.el\" (list (car 
load-path))) (load \"local-environment.el\" nil t t))" \
        --eval "(setq byte-compile-error-on-warn t)"                            
                                              \
        --eval "(batch-byte-compile)" datetime.el
diff --git a/test/FormatTimestamp.java b/test/FormatTimestamp.java
deleted file mode 100644
index e3da3dcc33..0000000000
--- a/test/FormatTimestamp.java
+++ /dev/null
@@ -1,36 +0,0 @@
-import java.time.*;
-import java.time.format.*;
-import java.util.*;
-
-
-public class FormatTimestamp
-{
-    /**
-     *  Usage (e.g.): echo TIMESTAMP TIMEZONE LOCALE PATTERN | java 
FormatTimestamp
-     *
-     *  where:
-     *    TIMESTAMP is a double number of seconds since epoch time UTC;
-     *    TIMEZONE and LOCALE are string identifiers;
-     *    PATTERN is according to SimpleDateFormat documentation and is taken 
until
-     *      the end of line with starting and ending whitespace removed.
-     *
-     *  The four tokens can be repeated as many times as needed.  Output is 
one formatted
-     *  timestamp per line, corresponding to each quadruplet in the input.
-     */
-    public static void main (String[] args) throws Exception
-    {
-        Scanner  input = new Scanner (System.in).useLocale (Locale.ENGLISH);
-
-        while (input.hasNext ()) {
-            double  timestamp = input.nextDouble ();
-            ZoneId  timezone  = ZoneId.of (input.next ());
-            Locale  locale    = Locale.forLanguageTag (input.next ());
-            String  pattern   = input.nextLine ().trim ();
-
-            System.out.println (DateTimeFormatter.ofPattern (pattern, locale)
-                                .format (LocalDateTime.ofInstant 
(Instant.ofEpochSecond ((long) Math.floor (timestamp),
-                                                                               
          (int) Math.floor ((timestamp - Math.floor (timestamp))  * 
1_000_000_000)),
-                                                                  timezone)));
-        }
-    }
-}
diff --git a/test/ProcessTimestamp.java b/test/ProcessTimestamp.java
new file mode 100644
index 0000000000..16202b688f
--- /dev/null
+++ b/test/ProcessTimestamp.java
@@ -0,0 +1,108 @@
+import java.time.*;
+import java.time.format.*;
+import java.time.temporal.*;
+import java.util.*;
+
+
+public class ProcessTimestamp
+{
+    /**
+     *  Usage (e.g.):
+     *      echo format TIMESTAMP TIMEZONE LOCALE PATTERN | java 
ProcessTimestamp
+     *  or:
+     *      echo -e parse FORMATTED "\n" TIMEZONE LOCALE PATTERN | java 
ProcessTimestamp
+     *
+     *  where:
+     *    TIMESTAMP is (only for command `format'): a double number of seconds 
since
+     *      epoch time UTC;
+     *    FORMATTED is (only for command `parse'): a timestamp formatted 
according to
+     *      the parameters that follow; read until linefeed;
+     *    TIMEZONE and LOCALE are string identifiers;
+     *    PATTERN is according to SimpleDateFormat documentation and is taken 
until
+     *      the end of line with starting and ending whitespace removed.
+     *
+     *  The five tokens can be repeated as many times as needed.  Output is 
either one
+     *  formatted timestamp or one double number (depending on the command) 
per line,
+     *  corresponding to each five tokens in the input.
+     */
+    public static void main (String[] args) throws Exception
+    {
+        Scanner  input = new Scanner (System.in).useLocale (Locale.ENGLISH);
+
+        while (input.hasNext ()) {
+            String  command = input.next ();
+            if (!"format".equals (command) && !"parse".equals (command))
+                throw new IllegalArgumentException (String.format ("unknown 
command '%s'", command));
+
+            double  timestamp = ("format".equals (command) ? input.nextDouble 
()       : 0.0);
+            String  formatted = ("parse" .equals (command) ? input.nextLine 
().trim () : null);
+            ZoneId  timezone  = ZoneId.of (input.next ());
+            Locale  locale    = Locale.forLanguageTag (input.next ());
+            String  pattern   = input.nextLine ().trim ();
+
+            switch (command) {
+            case "format":
+                System.out.println (DateTimeFormatter.ofPattern (pattern, 
locale)
+                                    .format (LocalDateTime.ofInstant 
(Instant.ofEpochSecond ((long) Math.floor (timestamp),
+                                                                               
              (int) Math.floor ((timestamp - Math.floor (timestamp))  * 
1_000_000_000)),
+                                                                      
timezone)));
+                break;
+
+            case "parse":
+                DateTimeFormatterBuilder  builder = (new 
DateTimeFormatterBuilder ()
+                                                     .parseCaseInsensitive ()
+                                                     // Commented out since it 
triggers bugs in obscure locales in Java.
+                                                     // We don't use this for 
testing anyway.
+                                                     // .parseLenient ()  
+                                                     .appendPattern (pattern));
+
+                try {
+                    // Apparently we cannot blindly set default values as they 
will
+                    // conflict with actually parsed values (at least in some 
cases).
+                    // This is not what I'd call defaults, but oh well...
+                    TemporalAccessor  parsed = builder.toFormatter 
(locale).parse (formatted);
+
+                    if (!parsed.isSupported (ChronoField.YEAR))
+                        builder.parseDefaulting (ChronoField.YEAR, 1970);
+
+                    if (!parsed.isSupported (ChronoField.MONTH_OF_YEAR))
+                        builder.parseDefaulting (ChronoField.MONTH_OF_YEAR, 1);
+
+                    if (!parsed.isSupported (ChronoField.DAY_OF_MONTH))
+                        builder.parseDefaulting (ChronoField.DAY_OF_MONTH, 1);
+
+                    if (!parsed.isSupported (ChronoField.HOUR_OF_DAY))
+                        builder.parseDefaulting (ChronoField.HOUR_OF_DAY, 0);
+
+                    if (!parsed.isSupported (ChronoField.MINUTE_OF_HOUR))
+                        builder.parseDefaulting (ChronoField.MINUTE_OF_HOUR, 
0);
+
+                    if (!parsed.isSupported (ChronoField.SECOND_OF_MINUTE))
+                        builder.parseDefaulting (ChronoField.SECOND_OF_MINUTE, 
0);
+
+                    if (!parsed.isSupported (ChronoField.NANO_OF_SECOND))
+                        builder.parseDefaulting (ChronoField.NANO_OF_SECOND, 
0);
+
+                    Instant  result = (builder
+                                       .toFormatter (locale).withZone 
(timezone)
+                                       .parse (formatted, Instant::from));
+
+                    long  seconds = result.getEpochSecond ();
+                    int   nano    = result.getNano ();
+
+                    if (seconds < 0 && nano != 0) {
+                        seconds += 1;
+                        nano     = 1_000_000_000 - nano;
+                    }
+
+                    System.out.println (String.format ("%d.%09d", seconds, 
nano).replaceFirst ("(?<!\\.)0*$", ""));
+                }
+                catch (DateTimeParseException exception) {
+                    System.out.println ("nil");
+                }
+
+                break;
+            }
+        }
+    }
+}
diff --git a/test/format.el b/test/format.el
index 341df7d19d..4562979bc3 100644
--- a/test/format.el
+++ b/test/format.el
@@ -19,75 +19,38 @@
 (require 'datetime)
 (require 'ert)
 
-
-(defvar datetime--test-timezone  nil)
-(defvar datetime--test-locale    nil)
-(defvar datetime--test-pattern   nil)
 (defvar datetime--test-formatter nil)
 
-(defvar datetime--test-java-formatting-process nil)
-
-(defvar datetime--test-directory (file-name-directory (or load-file-name 
(buffer-file-name))))
 
-
-(defmacro datetime--test-set-up (timezone locale pattern &rest body)
+(defmacro datetime--test-set-up-formatter (timezone locale pattern &rest body)
   (declare (debug (form form form body))
            (indent 3))
-  `(let* ((datetime--test-timezone  ,timezone)
-          (datetime--test-locale    ,locale)
-          (datetime--test-pattern   ,pattern)
-          (datetime--test-formatter (datetime-float-formatter 'java 
datetime--test-pattern :timezone datetime--test-timezone :locale 
datetime--test-locale)))
-     ,@body))
-
-;; We assume that the Java program is already compiled externally (see 
`run-tests.sh').
-(defun datetime--test (times)
+  `(datetime--test-set-up ,timezone ,locale ,pattern
+     (let ((datetime--test-formatter (datetime-float-formatter 'java 
datetime--test-pattern :timezone datetime--test-timezone :locale 
datetime--test-locale)))
+       ,@body)))
+
+(defun datetime--test-formatter (times)
   (unless (listp times)
     (setq times (list times)))
-  (unless (process-live-p datetime--test-java-formatting-process)
-    (let ((default-directory datetime--test-directory))
-      (setq datetime--test-java-formatting-process (make-process :name 
"java-formatter" :buffer "java-formatter" :stderr "java-formatter/stderr"
-                                                                 :command 
'("java" "FormatTimestamp")))))
-  (let* ((marker        (process-mark datetime--test-java-formatting-process))
-         (position      (marker-position marker))
-         (num-times     (length times))
-         (num-formatted 0)
-         formatted)
-    (save-excursion
-      (set-buffer (marker-buffer marker))
-      ;; It is much faster to give "tasks" to the remote process in
-      ;; batch, then fetch the results.
-      (dolist (time times)
-        (process-send-string datetime--test-java-formatting-process
-                             (format "%s %s %s %s\n" time 
datetime--test-timezone datetime--test-locale datetime--test-pattern)))
-      (while (< num-formatted num-times)
-        (while (or (= (marker-position marker) position) (/= (char-before 
marker) ?\n))
-          (accept-process-output datetime--test-java-formatting-process))
-        (while (> (marker-position marker) position)
-          (goto-char position)
-          (end-of-line)
-          (push (buffer-substring position (point)) formatted)
-          (beginning-of-line 2)
-          (setq position      (point)
-                num-formatted (1+ num-formatted))))
-      (setq formatted (nreverse formatted))
-      (while times
-        (let ((time     (pop times))
-              (expected (pop formatted)))
-          (eval `(should (progn ',datetime--test-timezone 
',datetime--test-locale ,datetime--test-pattern ,time
-                                (string= ,(funcall datetime--test-formatter 
time) ,expected)))))))))
-
-(defun datetime--test-transition (time)
-  (datetime--test (list time
-                        (+ time  0.5) (- time  0.5)     ; half a second
-                        (+ time   30) (- time   30)     ; half a minute
-                        (+ time 1800) (- time 1800)     ; half an hour
-                        (+ time 3600) (- time 3600)     ; one hour
-                        (+ time 7200) (- time 7200))))  ; two hours
+  (let ((formatted (datetime--test 'format times)))
+    (while times
+      (let ((time     (pop times))
+            (expected (pop formatted)))
+        (eval `(should (progn ',datetime--test-timezone 
',datetime--test-locale ,datetime--test-pattern ,time
+                              (string= ,(funcall datetime--test-formatter 
time) ,expected))))))))
+
+(defun datetime--test-formatter-around-transition (time)
+  (datetime--test-formatter (list time
+                                  (+ time  0.5) (- time  0.5)     ; half a 
second
+                                  (+ time   30) (- time   30)     ; half a 
minute
+                                  (+ time 1800) (- time 1800)     ; half an 
hour
+                                  (+ time 3600) (- time 3600)     ; one hour
+                                  (+ time 7200) (- time 7200))))  ; two hours
 
 
 (ert-deftest datetime-test-formatting-now ()
-  (datetime--test-set-up 'UTC 'en "yyyy-MM-dd HH:mm:ss.SSS"
-    (datetime--test (float-time))))
+  (datetime--test-set-up-formatter 'UTC 'en "yyyy-MM-dd HH:mm:ss.SSS"
+    (datetime--test-formatter (float-time))))
 
 (ert-deftest datetime-test-formatting-now-standard-formats ()
   (let ((now (float-time)))
@@ -95,60 +58,60 @@
       (dolist (variant '(:short :medium :long :full))
         (let ((pattern (datetime-locale-date-time-pattern locale variant)))
           (unless (datetime-pattern-includes-timezone-p 'java pattern)
-            (datetime--test-set-up 'UTC locale pattern
-              (datetime--test now))))))))
+            (datetime--test-set-up-formatter 'UTC locale pattern
+              (datetime--test-formatter now))))))))
 
 (ert-deftest datetime-test-formatting-various-timestamps-1 ()
-  (datetime--test-set-up 'UTC 'en "yyyy-MM-dd HH:mm:ss.SSS"
+  (datetime--test-set-up-formatter 'UTC 'en "yyyy-MM-dd HH:mm:ss.SSS"
     ;; Roughly from 400 AD till 3500 AD with 4 month step.
-    (datetime--test (mapcar (lambda (k) (* k 10000000.123)) (number-sequence 
-5000 5000)))))
+    (datetime--test-formatter (mapcar (lambda (k) (* k 10000000.123)) 
(number-sequence -5000 5000)))))
 
 (ert-deftest 
datetime-test-formatting-various-timestamps-with-fixed-offset-timezone-1 ()
-  (datetime--test-set-up 'Etc/GMT+1 'en "yyyy-MM-dd HH:mm:ss.SSS"
+  (datetime--test-set-up-formatter 'Etc/GMT+1 'en "yyyy-MM-dd HH:mm:ss.SSS"
     ;; Roughly from 400 AD till 3500 AD with 4 month step.
-    (datetime--test (mapcar (lambda (k) (* k 10000000.123)) (number-sequence 
-5000 5000)))))
+    (datetime--test-formatter (mapcar (lambda (k) (* k 10000000.123)) 
(number-sequence -5000 5000)))))
 
 (ert-deftest 
datetime-test-formatting-various-timestamps-with-shifting-timezone-1 ()
-  (datetime--test-set-up 'Europe/Madrid 'en "yyyy-MM-dd HH:mm:ss.SSS"
+  (datetime--test-set-up-formatter 'Europe/Madrid 'en "yyyy-MM-dd HH:mm:ss.SSS"
     ;; Roughly from 400 AD till 3500 AD with 4 month step.
-    (datetime--test (mapcar (lambda (k) (* k 10000000.123)) (number-sequence 
-5000 5000)))))
+    (datetime--test-formatter (mapcar (lambda (k) (* k 10000000.123)) 
(number-sequence -5000 5000)))))
 
 (ert-deftest 
datetime-test-formatting-various-timestamps-with-shifting-timezone-2 ()
-  (datetime--test-set-up 'America/Anchorage 'en "yyyy-MM-dd HH:mm:ss.SSS"
+  (datetime--test-set-up-formatter 'America/Anchorage 'en "yyyy-MM-dd 
HH:mm:ss.SSS"
     ;; Roughly from 400 AD till 3500 AD with 4 month step.
-    (datetime--test (mapcar (lambda (k) (* k 10000000.123)) (number-sequence 
-5000 5000)))))
+    (datetime--test-formatter (mapcar (lambda (k) (* k 10000000.123)) 
(number-sequence -5000 5000)))))
 
 (ert-deftest 
datetime-test-formatting-various-timestamps-with-shifting-timezone-3 ()
-  (datetime--test-set-up 'Australia/Hobart 'en "yyyy-MM-dd HH:mm:ss.SSS"
+  (datetime--test-set-up-formatter 'Australia/Hobart 'en "yyyy-MM-dd 
HH:mm:ss.SSS"
     ;; Roughly from 400 AD till 3500 AD with 4 month step.
-    (datetime--test (mapcar (lambda (k) (* k 10000000.123)) (number-sequence 
-5000 5000)))))
+    (datetime--test-formatter (mapcar (lambda (k) (* k 10000000.123)) 
(number-sequence -5000 5000)))))
 
 (ert-deftest datetime-test-formatting-text-1 ()
-  (datetime--test-set-up 'UTC 'en "'on' EEEE 'the' d MMMM 'of' yyyy G, 'at' 
h:mm:ss a"
+  (datetime--test-set-up-formatter 'UTC 'en "'on' EEEE 'the' d MMMM 'of' yyyy 
G, 'at' h:mm:ss a"
     ;; Roughly from 1200 BC till 5100 AD with 6 and a half year step.
-    (datetime--test (mapcar (lambda (k) (* k 200000000.123)) (number-sequence 
-500 500)))))
+    (datetime--test-formatter (mapcar (lambda (k) (* k 200000000.123)) 
(number-sequence -500 500)))))
 
 (ert-deftest datetime-test-formatting-around-offset-transition-1 ()
-  (datetime--test-set-up 'Europe/Madrid 'en "yyyy-MM-dd HH:mm:ss.SSS"
+  (datetime--test-set-up-formatter 'Europe/Madrid 'en "yyyy-MM-dd HH:mm:ss.SSS"
     ;; First historical transition.
-    (datetime--test-transition -2177452800)))
+    (datetime--test-formatter-around-transition -2177452800)))
 
 (ert-deftest datetime-test-formatting-around-offset-transition-2 ()
-  (datetime--test-set-up 'Europe/Madrid 'en "yyyy-MM-dd HH:mm:ss.SSS"
+  (datetime--test-set-up-formatter 'Europe/Madrid 'en "yyyy-MM-dd HH:mm:ss.SSS"
     ;; Rule-based transition on 2010-03-25.
-    (datetime--test-transition 1269738000)))
+    (datetime--test-formatter-around-transition 1269738000)))
 
 (ert-deftest datetime-test-formatting-around-offset-transition-3 ()
-  (datetime--test-set-up 'Europe/Madrid 'en "yyyy-MM-dd HH:mm:ss.SSS"
+  (datetime--test-set-up-formatter 'Europe/Madrid 'en "yyyy-MM-dd HH:mm:ss.SSS"
     ;; Future transition on 2480-10-27 (according to the rules as of 2018).
-    (datetime--test-transition 16119997200)))
+    (datetime--test-formatter-around-transition 16119997200)))
 
 (ert-deftest datetime-test-formatting-around-offset-transition-4 ()
-  (datetime--test-set-up 'America/Anchorage 'en "yyyy-MM-dd HH:mm:ss.SSS"
+  (datetime--test-set-up-formatter 'America/Anchorage 'en "yyyy-MM-dd 
HH:mm:ss.SSS"
     ;; Rule-based transition on 2009-03-08.
-    (datetime--test-transition 1236510000)))
+    (datetime--test-formatter-around-transition 1236510000)))
 
 (ert-deftest datetime-test-formatting-around-offset-transition-5 ()
-  (datetime--test-set-up 'Australia/Hobart 'en "yyyy-MM-dd HH:mm:ss.SSS"
+  (datetime--test-set-up-formatter 'Australia/Hobart 'en "yyyy-MM-dd 
HH:mm:ss.SSS"
     ;; Rule-based transition on 2014-10-05.
-    (datetime--test-transition 1412438400)))
+    (datetime--test-formatter-around-transition 1412438400)))
diff --git a/test/parse.el b/test/parse.el
new file mode 100644
index 0000000000..beb4d9f694
--- /dev/null
+++ b/test/parse.el
@@ -0,0 +1,141 @@
+;;; -*- lexical-binding: t -*-
+
+;; Copyright (C) 2018 Paul Pogonyshev
+
+;; This program is free software; you can redistribute it and/or
+;; modify it under the terms of the GNU General Public License as
+;; published by the Free Software Foundation, either version 3 of
+;; the License, or (at your option) any later version.
+
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+;;
+;; You should have received a copy of the GNU General Public License
+;; along with this program.  If not, see http://www.gnu.org/licenses.
+
+
+(require 'datetime)
+(require 'ert)
+
+(defvar datetime--test-parser nil)
+
+
+(defun datetime--~= (a b &optional epsilon)
+  (when (and a b)
+    (unless (<= (abs (- a b)) (or epsilon 0.0000001))
+      (message "Error of %s" (funcall (datetime-float-formatter 'java 
"HH:mm:ss.SSSSSS" :second-fractional-extension t) (abs (- a b))))))
+  (if (and a b)
+      (<= (abs (- a b)) (or epsilon 0.0000001))
+    (not (or a b))))
+
+(defmacro datetime--test-set-up-parser (timezone locale pattern &rest body)
+  (declare (debug (form form form body))
+           (indent 3))
+  `(datetime--test-set-up ,timezone ,locale ,pattern
+     (let ((datetime--test-parser (datetime-parser-to-float 'java 
datetime--test-pattern :timezone datetime--test-timezone :locale 
datetime--test-locale)))
+       ,@body)))
+
+(defun datetime--test-parser (as-strings)
+  (unless (listp as-strings)
+    (setq as-strings (list as-strings)))
+  (let ((parsed (datetime--test 'parse as-strings)))
+    (while as-strings
+      (let ((as-string (pop as-strings))
+            (time      (pop parsed)))
+        (eval `(should (progn ',datetime--test-timezone 
',datetime--test-locale ,datetime--test-pattern ,as-string
+                              (datetime--~= ,(funcall datetime--test-parser 
as-string) ,time))))))))
+
+(defun datetime--test-parser-around-transition (time)
+  (datetime--test-parser (datetime--test 'format (list time
+                                                       (+ time  0.5) (- time  
0.5)      ; half a second
+                                                       (+ time   30) (- time   
30)      ; half a minute
+                                                       (+ time 1800) (- time 
1800)      ; half an hour
+                                                       (+ time 3600) (- time 
3600)      ; one hour
+                                                       (+ time 7200) (- time 
7200)))))  ; two hours
+
+
+(ert-deftest datetime-test-parsing-now ()
+  (datetime--test-set-up-parser 'UTC 'en "yyyy-MM-dd HH:mm:ss.SSS"
+    (datetime--test-parser (datetime--test 'format (float-time)))))
+
+(ert-deftest datetime-test-parsing-now-standard-formats ()
+  (let ((now (float-time)))
+    (dolist (locale (datetime-list-locales t))
+      (dolist (variant '(:short :medium :long :full))
+        (let ((pattern (datetime-locale-date-time-pattern locale variant)))
+          (unless (datetime-pattern-includes-timezone-p 'java pattern)
+            (datetime--test-set-up-parser 'UTC locale pattern
+              (datetime--test-parser (datetime--test 'format now)))))))))
+
+(ert-deftest datetime-test-parsing-various-timestamps-1 ()
+  (datetime--test-set-up-parser 'UTC 'en "yyyy-MM-dd HH:mm:ss.SSS"
+    ;; Roughly from 400 AD till 3500 AD with 4 month step.
+    (datetime--test-parser (datetime--test 'format (mapcar (lambda (k) (* k 
10000000.123)) (number-sequence -5000 5000))))))
+
+(ert-deftest 
datetime-test-parsing-various-timestamps-with-fixed-offset-timezone-1 ()
+  (datetime--test-set-up-parser 'Etc/GMT+1 'en "yyyy-MM-dd HH:mm:ss.SSS"
+    ;; Roughly from 400 AD till 3500 AD with 4 month step.
+    (datetime--test-parser (datetime--test 'format (mapcar (lambda (k) (* k 
10000000.123)) (number-sequence -5000 5000))))))
+
+(ert-deftest datetime-test-parsing-various-timestamps-with-shifting-timezone-1 
()
+  (datetime--test-set-up-parser 'Europe/Madrid 'en "yyyy-MM-dd HH:mm:ss.SSS"
+    ;; Roughly from 400 AD till 3500 AD with 4 month step.
+    (datetime--test-parser (datetime--test 'format (mapcar (lambda (k) (* k 
10000000.123)) (number-sequence -5000 5000))))))
+
+(ert-deftest datetime-test-parsing-various-timestamps-with-shifting-timezone-2 
()
+  (datetime--test-set-up-parser 'America/Anchorage 'en "yyyy-MM-dd 
HH:mm:ss.SSS"
+    ;; Roughly from 400 AD till 3500 AD with 4 month step.
+    (datetime--test-parser (datetime--test 'format (mapcar (lambda (k) (* k 
10000000.123)) (number-sequence -5000 5000))))))
+
+(ert-deftest datetime-test-parsing-various-timestamps-with-shifting-timezone-3 
()
+  (datetime--test-set-up-parser 'Australia/Hobart 'en "yyyy-MM-dd HH:mm:ss.SSS"
+    ;; Roughly from 400 AD till 3500 AD with 4 month step.
+    (datetime--test-parser (datetime--test 'format (mapcar (lambda (k) (* k 
10000000.123)) (number-sequence -5000 5000))))))
+
+(ert-deftest datetime-test-parsing-text-1 ()
+  (datetime--test-set-up-parser 'UTC 'en "'on' EEEE 'the' d MMMM 'of' yyyy G, 
'at' h:mm:ss a"
+    ;; Roughly from 1200 BC till 5100 AD with 6 and a half year step.
+    (datetime--test-parser (datetime--test 'format (mapcar (lambda (k) (* k 
200000000.123)) (number-sequence -500 500))))))
+
+(ert-deftest datetime-test-parsing-around-offset-transition-1 ()
+  (datetime--test-set-up-parser 'Europe/Madrid 'en "yyyy-MM-dd HH:mm:ss.SSS"
+    ;; First historical transition.
+    (datetime--test-parser-around-transition -2177452800)))
+
+(ert-deftest datetime-test-parsing-around-offset-transition-2 ()
+  (datetime--test-set-up-parser 'Europe/Madrid 'en "yyyy-MM-dd HH:mm:ss.SSS"
+    ;; Rule-based transition on 2010-03-25.
+    (datetime--test-parser-around-transition 1269738000)))
+
+(ert-deftest datetime-test-parsing-around-offset-transition-3 ()
+  (datetime--test-set-up-parser 'Europe/Madrid 'en "yyyy-MM-dd HH:mm:ss.SSS"
+    ;; Future transition on 2480-10-27 (according to the rules as of 2018).
+    (datetime--test-parser-around-transition 16119997200)))
+
+(ert-deftest datetime-test-parsing-around-offset-transition-4 ()
+  (datetime--test-set-up-parser 'America/Anchorage 'en "yyyy-MM-dd 
HH:mm:ss.SSS"
+    ;; Rule-based transition on 2009-03-08.
+    (datetime--test-parser-around-transition 1236510000)))
+
+(ert-deftest datetime-test-parsing-around-offset-transition-5 ()
+  (datetime--test-set-up-parser 'Australia/Hobart 'en "yyyy-MM-dd HH:mm:ss.SSS"
+    ;; Rule-based transition on 2014-10-05.
+    (datetime--test-parser-around-transition 1412438400)))
+
+(ert-deftest datetime-test-parser-validating-1 ()
+  (datetime--test-set-up-parser 'UTC 'en "yyyy-MM-dd HH:mm:ss.SSS"
+    (should-error (funcall datetime--test-parser "lol") :type 
'datetime-invalid-string)
+    (should-error (funcall datetime--test-parser "2000-00-01 00:00:00.000") 
:type 'datetime-invalid-string)
+    (should-error (funcall datetime--test-parser "2000-13-01 00:00:00.000") 
:type 'datetime-invalid-string)
+    (should-error (funcall datetime--test-parser "2000-01-00 00:00:00.000") 
:type 'datetime-invalid-string)
+    (should-error (funcall datetime--test-parser "2000-01-32 00:00:00.000") 
:type 'datetime-invalid-string)
+    (should-error (funcall datetime--test-parser "2000-01-01 24:00:00.000") 
:type 'datetime-invalid-string)
+    (should-error (funcall datetime--test-parser "2000-01-01 00:60:00.000") 
:type 'datetime-invalid-string)
+    (should-error (funcall datetime--test-parser "2000-01-01 00:00:60.000") 
:type 'datetime-invalid-string)))
+
+(ert-deftest datetime-test-parser-validating-excessive-patterns ()
+  (datetime--test-set-up-parser 'UTC 'en "dd 'of' MMMM '(month' M')'"
+    (datetime--test-parser "12 of March (month 3)")
+    (should-error (funcall datetime--test-parser "12 of March (month 1)") 
:type 'datetime-invalid-string)))
diff --git a/test/test.el b/test/test.el
new file mode 100644
index 0000000000..c52cbdfb01
--- /dev/null
+++ b/test/test.el
@@ -0,0 +1,70 @@
+;;; -*- lexical-binding: t -*-
+
+;; Copyright (C) 2018 Paul Pogonyshev
+
+;; This program is free software; you can redistribute it and/or
+;; modify it under the terms of the GNU General Public License as
+;; published by the Free Software Foundation, either version 3 of
+;; the License, or (at your option) any later version.
+
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+;;
+;; You should have received a copy of the GNU General Public License
+;; along with this program.  If not, see http://www.gnu.org/licenses.
+
+
+(require 'datetime)
+(require 'ert)
+
+
+(defvar datetime--test-timezone nil)
+(defvar datetime--test-locale   nil)
+(defvar datetime--test-pattern  nil)
+
+(defvar datetime--test-java-process nil)
+
+(defvar datetime--test-directory (file-name-directory (or load-file-name 
(buffer-file-name))))
+
+
+(defmacro datetime--test-set-up (timezone locale pattern &rest body)
+  (declare (debug (form form form body))
+           (indent 3))
+  `(let ((datetime--test-timezone  ,timezone)
+         (datetime--test-locale    ,locale)
+         (datetime--test-pattern   ,pattern))
+     ,@body))
+
+(defun datetime--test (command times)
+  (unless (listp times)
+    (setq times (list times)))
+  (unless (process-live-p datetime--test-java-process)
+    (let ((default-directory datetime--test-directory))
+      (setq datetime--test-java-process (make-process :name "java-benchmark" 
:buffer "java-benchmark" :stderr "java-benchmark/stderr"
+                                                      :command '("java" 
"ProcessTimestamp")))))
+  (let* ((marker           (process-mark datetime--test-java-process))
+         (position         (marker-position marker))
+         (num-times        (length times))
+         (num-result-lines 0)
+         result)
+    (save-excursion
+      (set-buffer (marker-buffer marker))
+      ;; It is much faster to give "tasks" to the remote process in
+      ;; batch, then fetch the results.
+      (dolist (time times)
+        (process-send-string datetime--test-java-process
+                             (format "%s %s\n%s %s %s\n" command time 
datetime--test-timezone datetime--test-locale datetime--test-pattern)))
+      (while (< num-result-lines num-times)
+        (while (or (= (marker-position marker) position) (/= (char-before 
marker) ?\n))
+          (accept-process-output datetime--test-java-process))
+        (while (> (marker-position marker) position)
+          (goto-char position)
+          (end-of-line)
+          (let ((as-string (buffer-substring position (point))))
+            (push (if (eq command 'format) as-string (car (read-from-string 
as-string))) result))
+          (beginning-of-line 2)
+          (setq position         (point)
+                num-result-lines (1+ num-result-lines))))
+      (nreverse result))))

Reply via email to