diff --git a/CHANGELOG b/CHANGELOG index 370675d18ab9ad7b2297ded26904c1cd4ef79cbf..0261a616cc4d2063c2d55ffdc9a39029e8afec3f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,13 @@ +2020-07-23 David Byers <david.byers@liu.se> + + Fix issue #1 (olc-recover-compound fails on full codes): + * test/olctest.el: Improved test macros. Tests for issue 1. + + * olc.el (general): Whitespace fixes. + (olc-recover-error): New error. + (olc-recover-compound): New argument list. Type checks. Handle + full codes being passed. + 2020-07-22 David Byers <david.byers@liu.se> Fix test cases: diff --git a/Makefile b/Makefile index c760d85a20975aa2069a64e5d9852841736e890a..e84406c2838450399eb0e7c88d0abafb2e8e1f2f 100644 --- a/Makefile +++ b/Makefile @@ -22,6 +22,17 @@ all: olc.elc olc.info +check: + emacs --batch \ + --eval "(setq-default indent-tabs-mode nil)" \ + -f package-initialize \ + -l elisp-lint \ + -f elisp-lint-files-batch \ + --no-check-declare \ + --no-indent \ + olc.el ; \ + rm -f olc-autoloads.el olc-autoloads.el~ + olc.elc: olc.el emacs --batch -f batch-byte-compile olc.el diff --git a/olc.el b/olc.el index a43a90a183c2cb9623e360662e0bb316d9f1d9ba..0d59bad7638ec66f7d3abf424a88b4d4a2ac7ac5 100644 --- a/olc.el +++ b/olc.el @@ -3,7 +3,7 @@ ;; Copyright (C) 2020 David Byers ;; ;; Author: David Byers <david.byers@liu.se> -;; Version: 1.0 +;; Version: 1.0.1 ;; Package-Requires: ((emacs "25.1")) ;; Keywords: extensions, lisp ;; URL: https://gitlab.liu.se/davby02/olc @@ -25,12 +25,12 @@ ;;; Commentary: ;; This program provides basic open location code support in Emacs -;; Lisp. The support for recovering shortened codes depends on the +;; Lisp. The support for recovering shortened codes depends on the ;; request library and uses OpenStreetMap; please check the terms of ;; use for the service to ensure that you remain compliant. ;; ;; All methods required by the open location code specification are -;; provided in some form. The implementation passed the tests present +;; provided in some form. The implementation passed the tests present ;; in the open location code github repository at the time of writing ;; almost cleanly -- there are some minor rounding issues in decode. @@ -65,6 +65,8 @@ "Error encoding open location code" 'olc-error) (define-error 'olc-shorten-error "Error shortening open location code" 'olc-error) +(define-error 'olc-recover-error + "Error recovering open location code" 'olc-error) ;;; Base 20 digits: @@ -354,13 +356,13 @@ values cannot (legally) be encoded to the selected length." (defun olc-decode (code) "Decode open location code CODE. -Returns an `olc-area' structure. Raises `olc-parse-error' if the +Returns an `olc-area' structure. Raises `olc-parse-error' if the code can't be parsed, and `olc-decode-error' if it can't be decoded (e.g. a padded shortened code, a padded code with grid coordinates, an empty code, and so forth). Since this function uses floating point calculations, the results -are not identical to e.g. the C++ reference implementation. The +are not identical to e.g. the C++ reference implementation. The differences, however, are extremely small." (let* ((parse (olc-parse-code code)) (latscale (* (expt 20 4) (expt 5 5))) @@ -469,7 +471,8 @@ faster. (while (< zoom-lo zoom-hi) (let* ((zoom (floor (+ zoom-lo zoom-hi) 2)) (resp (request-response-data - (request "https://nominatim.openstreetmap.org/reverse" + (request + "https://nominatim.openstreetmap.org/reverse" :params `((lat . ,(olc-area-lat area)) (lon . ,(olc-area-lon area)) (zoom . ,zoom) @@ -545,43 +548,58 @@ full open location code." (olc-encode lat lon :len (olc-parse-precision parse))))))) -(cl-defun olc-recover-compound (arg1 &optional arg2 &key (format 'area)) - "Recover a location from a short code and reference. +(cl-defun olc-recover-compound (code &key ref (format 'area)) + "Recover a location from a compound code CODE. -When called with one argument, ARG1, it must be a string -consisting of a shortened open location code followed by -whitespace and a geographical location. - -When called with two strings, ARG1 and ARG2, the first must be a -shortened open location code and the second if the geographical -location. +Optional keyword argument REF indicates the reference to use. If +not specified, the reference is assumed to be embedded into CODE. If FORMAT is `area' (or any other value), the returned value is an full open location code." - (unless (fboundp 'request) - (error "`request' library is not loaded")) - - (let (code reference) - (cond ((and (stringp arg1) (not (stringp arg2))) - (if (string-match "^\\(\\S-+\\)\\s-+\\(.*\\)$" arg1) - (setq code (match-string 1 arg1) - reference (match-string 2 arg1)) - (signal 'wrong-type-argument arg1))) - ((and (stringp arg1) (stringp arg2)) - (setq code arg1 reference arg2)) - (t (signal 'wrong-type-argument arg1))) + ;; Make sure we can do requests + (unless (fboundp 'request) (signal 'void-function 'request)) + + ;; Check types (defer check of ref + (cl-check-type code stringp) + (cl-check-type format (member latlon area nil)) + + (if (string-match "^\\(\\S-+\\)\\s-+\\(.*\\)$" code) + (progn (cl-check-type ref null) + (setq ref (match-string 2 code) + code (match-string 1 code))) + (cl-check-type ref stringp)) + + ;; If the code is full then return it + (if (olc-is-full code) + code (let ((resp (request "https://nominatim.openstreetmap.org/search" - :params `((q . ,reference) + :params `((q . ,ref) (format . "json") (limit . 1)) :parser #'json-read :sync t))) - (when (eq 200 (request-response-status-code resp)) - (let ((data (elt (request-response-data resp) 0))) - (olc-recover code - (string-to-number (alist-get 'lat data)) - (string-to-number (alist-get 'lon data)) - :format format)))))) + + ;; Check that we got a response + (unless (eq 200 (request-response-status-code resp)) + (signal 'olc-recover-error + (list "error decoding reference" + (request-response-status-code resp)))) + + (let* ((data (elt (request-response-data resp) 0)) + (lat (alist-get 'lat data)) + (lon (alist-get 'lon data))) + + ;; Check that we have a lat and lon + (unless (and lat lon) + (signal 'olc-recover-error + (list "reference location missing lat or lon" + data))) + + ;; Finally recover the code! + (olc-recover code + (string-to-number lat) + (string-to-number lon) + :format format))))) (provide 'olc) diff --git a/test/olctest.el b/test/olctest.el index ae67632c754f4a8b233077b2968d4aff0ccfb717..a1c02c16887d322fdc27d7ed8cba7e3007f87fbe 100644 --- a/test/olctest.el +++ b/test/olctest.el @@ -23,11 +23,62 @@ ;; it only affects decoding, that is an insignificant error level. (defvar olctest-decode-tolerance 0.0000000001) -(defvar $olctest-results) +(defvar --olctest-results) +(defvar --olctest-current-case) + + +(defmacro olctest-testcase (name &rest body) + "Set up an open location code test case." + (declare (indent 1) + (debug (form &rest form))) + `(let ((--olctest-results nil) + (--olctest-current-case ,name)) + (message "olctest running %s" ,name) + ,@body + (olctest-report-results --olctest-results))) + + +(cl-defun olctest-record-failure (&key exp act msg) + "Record a test failure." + (setq --olctest-results + (cons `((name . ,--olctest-current-case) + (msg . ,msg) + (exp . ,exp) + (act . ,act)) + --olctest-results))) + +(defun olctest-report-results (results) + "Report results from tests." + (if (null results) + t + (dolist (result results) + (princ (format "%s:%s: expected %s got %s\n" + (alist-get 'name result) + (or (alist-get 'msg result) "-") + (alist-get 'exp result) + (alist-get 'act result)))))) + +(cl-defun olctest-string= (&key exp act msg) + (unless (string= exp act) + (olctest-record-failure :exp exp :act act :msg msg))) + +(cl-defun olctest-equal (&key exp act msg) + (unless (equal exp act) + (olctest-record-failure :exp exp :act act :msg msg))) + +(cl-defmacro olctest-assert-error ((&key exp msg) &rest body) + (declare (indent 1)) + `(when (condition-case --olctest-caught-error + (progn ,@body t) + (,exp nil) + (error (olctest-record-failure :exp ',exp :act --olctest-caught-error :msg ,msg) nil)) + (olctest-record-failure :exp ',exp :act 'noerror :msg ,msg))) + (defun olctest-read-csv (filename) "Read a CSV file with test data." - (let ((buffer (generate-new-buffer "*olctest*"))) + (let ((buffer (generate-new-buffer "*olctest*")) + (lineno 1)) (unwind-protect (save-window-excursion (set-buffer buffer) @@ -38,27 +89,29 @@ (unless (re-search-forward "^# Format.*:$" nil t) (error "format line not found in test data")) (forward-line 1) + (setq lineno (1+ lineno)) (beginning-of-line) (looking-at "^# *\\(\\S-*\\)") (let ((columns (split-string (match-string 1) "," nil)) (cases nil)) (while (= 0 (forward-line 1)) + (setq lineno (1+ lineno)) (beginning-of-line) (cond ((looking-at "^#")) ((looking-at "^\\s-*$")) ((looking-at "^\\(\\S-+\\)$") (setq cases - (cons - (cl-mapcar (lambda (key val) - (cons (intern key) - (cond ((string-match "^[-0-9.]+$" val) - (string-to-number val)) - ((string= val "true") t) - ((string= val "false") nil) - (t val)))) - columns - (split-string (match-string 1) "," nil)) - cases))) + (cons `((lineno . , lineno) + ,@(cl-mapcar (lambda (key val) + (cons (intern key) + (cond ((string-match "^[-0-9.]+$" val) + (string-to-number val)) + ((string= val "true") t) + ((string= val "false") nil) + (t val)))) + columns + (split-string (match-string 1) "," nil))) + cases))) (t (error (format "unable to parse test data: %s" (buffer-substring (point) @@ -72,12 +125,11 @@ \(fn (VAR LIST) BODY...)" (declare (indent 1) (debug ((form symbolp) body))) - (let ((data (gensym "$olctest"))) - `(let* ((,data (olctest-read-csv ,(elt spec 0))) - ($olctest-results nil)) + (let ((data (gensym "---olctest"))) + `(let* ((,data (olctest-read-csv ,(elt spec 0)))) (dolist (,(elt spec 1) ,data) - ,@body) - (olctest-report-results $olctest-results)))) + ,@body)))) + (defmacro olctest-run-list (spec &rest body) "Run open location code tests. @@ -91,93 +143,82 @@ ,@body) (olctest-report-results $olctest-results)))) -(defun olctest-record-failure (case expected actual) - "Record a test failure." - (setq $olctest-results (cons `((case . ,case) - (expected . ,expected) - (actual . ,actual)) - $olctest-results))) - -(defun olctest-report-results (results) - "Report results from tests." - (if (null results) - t - (dolist (result results) - (princ (format "%s: expected %s got %s\n" - (mapconcat (lambda (val) (format "%s" (cdr val))) (alist-get 'case result) ",") - (alist-get 'expected result) - (alist-get 'actual result)))))) - (defun olctest-encode () "Test encoding." - (olctest-run-csv ("encoding.csv" case) - (let ((code (olc-encode (alist-get 'latitude case) - (alist-get 'longitude case) - :len (alist-get 'length case)))) - (unless (string= code (alist-get 'expected case)) - (olctest-record-failure case (alist-get 'expected case) code))))) + (olctest-testcase "reference:encoding" + (olctest-run-csv ("encoding.csv" case) + (let ((code (olc-encode (alist-get 'latitude case) + (alist-get 'longitude case) + :len (alist-get 'length case)))) + (olctest-string= :act code + :exp (alist-get 'expected case) + :msg (alist-get 'msg case)))))) (defun olctest-decode () "Test decoding." - (olctest-run-csv ("decoding.csv" case) - (let ((area (olc-decode (alist-get 'code case))) - (exp-latlo (alist-get 'latLo case)) - (exp-lathi (alist-get 'latHi case)) - (exp-lonlo (alist-get 'lngLo case)) - (exp-lonhi (alist-get 'lngHi case)) - (exp-len (alist-get 'length case))) - (unless (and (= exp-len (olc-code-precision (alist-get 'code case))) - (< (abs (- (olc-area-latlo area) exp-latlo)) olctest-decode-tolerance) - (< (abs (- (olc-area-lathi area) exp-lathi)) olctest-decode-tolerance) - (< (abs (- (olc-area-lonlo area) exp-lonlo)) olctest-decode-tolerance) - (< (abs (- (olc-area-lonhi area) exp-lonhi)) olctest-decode-tolerance)) - (olctest-record-failure case - (format "%d,%f,%f,%f,%f" exp-len exp-latlo exp-lonlo exp-lathi exp-lonhi) - (format "%d,%f,%f,%f,%f" - (olc-code-precision (alist-get 'code case)) - (olc-area-latlo area) - (olc-area-lonlo area) - (olc-area-lathi area) - (olc-area-lonhi area))))))) + (olctest-testcase "reference:decoding" + (olctest-run-csv ("decoding.csv" case) + (let ((area (olc-decode (alist-get 'code case))) + (exp-latlo (alist-get 'latLo case)) + (exp-lathi (alist-get 'latHi case)) + (exp-lonlo (alist-get 'lngLo case)) + (exp-lonhi (alist-get 'lngHi case)) + (exp-len (alist-get 'length case))) + (unless (and (= exp-len (olc-code-precision (alist-get 'code case))) + (< (abs (- (olc-area-latlo area) exp-latlo)) olctest-decode-tolerance) + (< (abs (- (olc-area-lathi area) exp-lathi)) olctest-decode-tolerance) + (< (abs (- (olc-area-lonlo area) exp-lonlo)) olctest-decode-tolerance) + (< (abs (- (olc-area-lonhi area) exp-lonhi)) olctest-decode-tolerance)) + (olctest-record-failure + :exp (format "%d,%f,%f,%f,%f" exp-len exp-latlo exp-lonlo exp-lathi exp-lonhi) + :act (format "%d,%f,%f,%f,%f" + (olc-code-precision (alist-get 'code case)) + (olc-area-latlo area) + (olc-area-lonlo area) + (olc-area-lathi area) + (olc-area-lonhi area)) + :msg (alist-get 'lineno case))))))) (defun olctest-shortcodes () "Test recovering." - (olctest-run-csv ("shortCodeTests.csv" case) - (let ((fullcode (alist-get 'fullcode case)) - (lat (alist-get 'lat case)) - (lon (alist-get 'lng case)) - (shortcode (alist-get 'shortcode case)) - (test-type (alist-get 'test_type case))) - - ;; Test recover - (when (or (string= test-type "B") (string= test-type "R")) - (let ((recovered (olc-recover shortcode lat lon))) - (unless (string= recovered fullcode) - (olctest-record-failure case fullcode recovered)))) - - ;; Test shorten - (when (or (string= test-type "B") (string= test-type "S")) - (let ((shortened (olc-shorten fullcode lat lon))) - (unless (string= shortened shortcode) - (olctest-record-failure case shortcode shortened)))) - ))) + (olctest-testcase "reference:shortcodes" + (olctest-run-csv ("shortCodeTests.csv" case) + (let ((fullcode (alist-get 'fullcode case)) + (lat (alist-get 'lat case)) + (lon (alist-get 'lng case)) + (shortcode (alist-get 'shortcode case)) + (test-type (alist-get 'test_type case)) + (lineno (alist-get 'lineno case))) + + ;; Test recover + (when (or (string= test-type "B") (string= test-type "R")) + (olctest-string= :act (olc-recover shortcode lat lon) + :exp fullcode + :msg lineno)) + + ;; Test shorten + (when (or (string= test-type "B") (string= test-type "S")) + (olctest-string= :act (olc-shorten fullcode lat lon) + :exp shortcode + :msg lineno)))))) (defun olctest-validity () "Test validity." - (olctest-run-csv ("validityTests.csv" case) - (let* ((code (alist-get 'code case)) - (expected (list (alist-get 'isValid case) - (alist-get 'isShort case) - (alist-get 'isFull case))) - (actual (list (not (not (olc-is-valid code))) - (not (not (olc-is-short code))) - (not (not (olc-is-full code)))))) - (unless (equal expected actual) - (olctest-record-failure case expected actual))))) + (olctest-testcase "reference:validity" + (olctest-run-csv ("validityTests.csv" case) + (let* ((code (alist-get 'code case)) + (exp (list (alist-get 'isValid case) + (alist-get 'isShort case) + (alist-get 'isFull case))) + (act (list (not (not (olc-is-valid code))) + (not (not (olc-is-short code))) + (not (not (olc-is-full code)))))) + (olctest-equal :exp exp :act act :msg code))))) + (defvar olctest-local-shorten-tests '(((code . "9C3W9QCJ+2VX") (lat . 51.3701125) (lon . -1.217765625) (len . 8) (exp . "+2VX")) @@ -186,15 +227,42 @@ ((code . "9C3W9QCJ+2VX") (lat . 51.3701125) (lon . -1.217765625) (len . 2) (exp . "3W9QCJ+2VX")))) (defun olctest-localtests () - (olctest-run-list (olctest-local-shorten-tests case) - (let* ((fullcode (alist-get 'code case)) - (lat (alist-get 'lat case)) - (lon (alist-get 'lon case)) - (len (alist-get 'len case)) - (shortcode (alist-get 'exp case)) - (actual (olc-shorten fullcode lat lon :limit len))) - (unless (string= actual shortcode) - (olctest-record-failure case shortcode actual))))) + (olctest-testcase "local:misc" + (olctest-run-list (olctest-local-shorten-tests case) + (let* ((fullcode (alist-get 'code case)) + (lat (alist-get 'lat case)) + (lon (alist-get 'lon case)) + (len (alist-get 'len case)) + (shortcode (alist-get 'exp case)) + (actual (olc-shorten fullcode lat lon :limit len))) + (olctest-string= :exp shortcode :act actual :msg len))))) + + +(defun olctest-issue-1 () + (olctest-testcase "local:issue-1" + (olctest-string= :exp "9FFV9VH8+9C" + :act (olc-recover-compound "9FFV9VH8+9C" :ref "Antarctica") + :msg "O1") + + (olctest-string= :exp "9FFPMGGC+9C" + :act (olc-recover-compound "+9C Sweden") + :msg "O2") + + (olctest-string= :exp "9FFPMGGC+9C" + :act (olc-recover-compound "+9C" :ref "Sweden") + :msg "O3") + + (olctest-assert-error (:exp (wrong-type-argument) :msg "F1") + (olc-recover-compound nil)) + + (olctest-assert-error (:exp (wrong-type-argument) :msg "F2") + (olc-recover-compound "+9C Sweden" :ref "Norway")) + + (olctest-assert-error (:exp (wrong-type-argument) :msg "F3") + (olc-recover-compound "+9C" :ref nil)) + + (olctest-assert-error (:exp (wrong-type-argument) :msg "F4") + (olc-recover-compound "+9C Sweden" :format 'undefined)))) (defun olctest-run-all () @@ -204,6 +272,7 @@ (olctest-shortcodes) (olctest-validity) (olctest-localtests) + (olctest-issue-1) )) (defun olctest-batch-test ()