├── README.md ├── package.lisp ├── README.mess ├── LICENSE ├── fuzzy-dates.asd ├── tz.txt ├── test.lisp ├── documentation.lisp ├── fuzzy-dates.lisp └── docs └── index.html /README.md: -------------------------------------------------------------------------------- 1 | # This repository has [moved](https://shinmera.com/projects/fuzzy-dates)! 2 | Due to Microsoft's continued enshittification of the platform this repository has been moved to [Codeberg](https://shinmera.com/projects/fuzzy-dates) in August of 2025. It will not receive further updates or patches. **Issues and pull requests will not be looked at here either**, please submit your patches and issue tickets on Codeberg, or send them directly via good old email patches to [shirakumo@tymoon.eu](mailto:shirakumo@tymoon.eu). 3 | 4 | Thanks. -------------------------------------------------------------------------------- /package.lisp: -------------------------------------------------------------------------------- 1 | (defpackage #:org.shirakumo.fuzzy-dates 2 | (:use #:cl) 3 | (:shadow #:print) 4 | (:export 5 | #:decode-weekday 6 | #:encode-weekday 7 | #:decode-month 8 | #:encode-month 9 | #:decode-unit 10 | #:decode-timezone 11 | #:decode-integer 12 | #:parse-forward-time 13 | #:parse-backward-time 14 | #:parse-date 15 | #:parse-rfc3339-like 16 | #:parse-iso8661-like 17 | #:parse-reverse-like 18 | #:parse-rfc1123-like 19 | #:parse-single 20 | #:parse 21 | #:print-rfc3339 22 | #:print-iso8661 23 | #:print-reverse 24 | #:print-rfc1123 25 | #:print-relative 26 | #:print-date 27 | #:print-clock 28 | #:print)) 29 | -------------------------------------------------------------------------------- /README.mess: -------------------------------------------------------------------------------- 1 | # About Fuzzy-Dates 2 | This is a small library to very fuzzily parse time and date strings into a universal-time timestamp. Among the fuzzily supported formats are: 3 | 4 | - Forwards and backwards relative stamps 5 | - Clocks 6 | - RFC1123 7 | - RFC3339 8 | - ISO8661 9 | - Weekdays and months 10 | - UNIX timestamps 11 | 12 | In short, it should parse a lot of what you throw at it and give you a reasonable timestamp for it. Note that while this does make this parsing rather suitable for parsing human input, there's still ambiguities that cannot be resolved, such as whether the user thinks dates are YMD, DMY, or MDY, and the human mind's ingenuity in coming up with new ways to write dates and times can't all be accounted for, so there are definitely timestamps that we can understand, which this library won't be able to parse or will parse different to how we understand it. 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023 Yukari Hafner 2 | 3 | This software is provided 'as-is', without any express or implied 4 | warranty. In no event will the authors be held liable for any damages 5 | arising from the use of this software. 6 | 7 | Permission is granted to anyone to use this software for any purpose, 8 | including commercial applications, and to alter it and redistribute it 9 | freely, subject to the following restrictions: 10 | 11 | 1. The origin of this software must not be misrepresented; you must not 12 | claim that you wrote the original software. If you use this software 13 | in a product, an acknowledgment in the product documentation would be 14 | appreciated but is not required. 15 | 2. Altered source versions must be plainly marked as such, and must not be 16 | misrepresented as being the original software. 17 | 3. This notice may not be removed or altered from any source distribution. 18 | -------------------------------------------------------------------------------- /fuzzy-dates.asd: -------------------------------------------------------------------------------- 1 | (asdf:defsystem fuzzy-dates 2 | :version "1.0.0" 3 | :license "zlib" 4 | :author "Yukari Hafner " 5 | :maintainer "Yukari Hafner " 6 | :description "A library to fuzzily parse date strings" 7 | :homepage "https://shinmera.com/docs/fuzzy-dates/" 8 | :bug-tracker "https://shinmera.com/project/fuzzy-dates/issues" 9 | :source-control (:git "https://shinmera.com/project/fuzzy-dates.git") 10 | :serial T 11 | :components ((:file "package") 12 | (:file "fuzzy-dates") 13 | (:file "documentation")) 14 | :depends-on (:cl-ppcre 15 | :documentation-utils) 16 | :in-order-to ((asdf:test-op (asdf:test-op :fuzzy-dates/test)))) 17 | 18 | (asdf:defsystem fuzzy-dates/test 19 | :version "1.0.0" 20 | :license "zlib" 21 | :author "Yukari Hafner " 22 | :maintainer "Yukari Hafner " 23 | :description "Tests for fuzzy-dates" 24 | :homepage "https://shinmera.com/docs/fuzzy-dates/" 25 | :bug-tracker "https://shinmera.com/project/fuzzy-dates/issues" 26 | :source-control (:git "https://shinmera.com/project/fuzzy-dates.git") 27 | :serial T 28 | :components ((:file "test")) 29 | :depends-on (:fuzzy-dates :parachute) 30 | :perform (asdf:test-op (op c) (uiop:symbol-call :parachute :test :org.shirakumo.fuzzy-dates.test))) 31 | -------------------------------------------------------------------------------- /tz.txt: -------------------------------------------------------------------------------- 1 | # Compiled on 15.9.2023 2 | ACDT +10:30 3 | ACST +09:30 4 | ACT -05 5 | ACT +08:00 6 | ACWST +08:45 7 | ADT -03 8 | AEDT +11 9 | AEST +10 10 | AFT +04:30 11 | AKDT -08 12 | AKST -09 13 | ALMT +06 14 | AMST -03 15 | AMT -04 16 | AMT +04 17 | ANAT +12 18 | AQTT +05 19 | ART -03 20 | AST +03 21 | AST -04 22 | AWST +08 23 | AZOST 00 24 | AZOT -01 25 | AZT +04 26 | BNT +08 27 | BIOT +06 28 | BIT -12 29 | BOT -04 30 | BRST -02 31 | BRT -03 32 | BST +06 33 | BST +11 34 | BST +01 35 | BTT +06 36 | CAT +02 37 | CCT +06:30 38 | CDT -05 39 | CDT -04 40 | CEST +02 41 | CET +01 42 | CHADT +13:45 43 | CHAST +12:45 44 | CHOT +08 45 | CHOST +09 46 | CHST +10 47 | CHUT +10 48 | CIST -08 49 | CKT -10 50 | CLST -03 51 | CLT -04 52 | COST -04 53 | COT -05 54 | CST -06 55 | CST +08 56 | CST -05 57 | CVT -01 58 | CWST +08:45 59 | CXT +07 60 | DAVT +07 61 | DDUT +10 62 | DFT +01 63 | EASST -05 64 | EAST -06 65 | EAT +03 66 | ECT -04 67 | ECT -05 68 | EDT -04 69 | EEST +03 70 | EET +02 71 | EGST 00 72 | EGT -01 73 | EST -05 74 | FET +03 75 | FJT +12 76 | FKST -03 77 | FKT -04 78 | FNT -02 79 | GALT -06 80 | GAMT -09 81 | GET +04 82 | GFT -03 83 | GILT +12 84 | GIT -09 85 | GMT 00 86 | GST -02 87 | GST +04 88 | GYT -04 89 | HDT -09 90 | HAEC +02 91 | HST -10 92 | HKT +08 93 | HMT +05 94 | HOVST +08 95 | HOVT +07 96 | ICT +07 97 | IDLW -12 98 | IDT +03 99 | IOT +03 100 | IRDT +04:30 101 | IRKT +08 102 | IRST +03:30 103 | IST +05:30 104 | IST +01 105 | IST +02 106 | JST +09 107 | KALT +02 108 | KGT +06 109 | KOST +11 110 | KRAT +07 111 | KST +09 112 | LHST +10:30 113 | LHST +11 114 | LINT +14 115 | MAGT +12 116 | MART -09:30 117 | MAWT +05 118 | MDT -06 119 | MET +01 120 | MEST +02 121 | MHT +12 122 | MIST +11 123 | MIT -09:30 124 | MMT +06:30 125 | MSK +03 126 | MST +08 127 | MST -07 128 | MUT +04 129 | MVT +05 130 | MYT +08 131 | NCT +11 132 | NDT -02:30 133 | NFT +11 134 | NOVT +07 135 | NPT +05:45 136 | NST -03:30 137 | NT -03:30 138 | NUT -11 139 | NZDT +13 140 | NZST +12 141 | OMST +06 142 | ORAT +05 143 | PDT -07 144 | PET -05 145 | PETT +12 146 | PGT +10 147 | PHOT +13 148 | PHT +08 149 | PHST +08 150 | PKT +05 151 | PMDT -02 152 | PMST -03 153 | PONT +11 154 | PST -08 155 | PWT +09 156 | PYST -03 157 | PYT -04 158 | RET +04 159 | ROTT -03 160 | SAKT +11 161 | SAMT +04 162 | SAST +02 163 | SBT +11 164 | SCT +04 165 | SDT -10 166 | SGT +08 167 | SLST +05:30 168 | SRET +11 169 | SRT -03 170 | SST -11 171 | SST +08 172 | SYOT +03 173 | TAHT -10 174 | THA +07 175 | TFT +05 176 | TJT +05 177 | TKT +13 178 | TLT +09 179 | TMT +05 180 | TRT +03 181 | TOT +13 182 | TST +08 183 | TVT +12 184 | ULAST +09 185 | ULAT +08 186 | UTC 00 187 | UYST -02 188 | UYT -03 189 | UZT +05 190 | VET -04 191 | VLAT +10 192 | VOLT +03 193 | VOST +06 194 | VUT +11 195 | WAKT +12 196 | WAST +02 197 | WAT +01 198 | WEST +01 199 | WET 00 200 | WIB +07 201 | WIT +09 202 | WITA +08 203 | WGST -02 204 | WGT -03 205 | WST +08 206 | YAKT +09 207 | YEKT +05 208 | -------------------------------------------------------------------------------- /test.lisp: -------------------------------------------------------------------------------- 1 | (defpackage #:org.shirakumo.fuzzy-dates.test 2 | (:use #:cl #:parachute) 3 | (:local-nicknames 4 | (#:dates #:org.shirakumo.fuzzy-dates)) 5 | (:export 6 | #:fuzzy-dates)) 7 | 8 | (in-package #:org.shirakumo.fuzzy-dates.test) 9 | 10 | (define-test fuzzy-dates) 11 | 12 | (define-test decoders 13 | :parent fuzzy-dates) 14 | 15 | (define-test weekday 16 | :parent decoders 17 | (is = 0 (dates:decode-weekday "mo")) 18 | (is = 1 (dates:decode-weekday "tue")) 19 | (is = 2 (dates:decode-weekday "wed")) 20 | (is = 3 (dates:decode-weekday "thursday")) 21 | (is = 4 (dates:decode-weekday "fr")) 22 | (is = 5 (dates:decode-weekday "sat")) 23 | (is = 6 (dates:decode-weekday "sunday"))) 24 | 25 | (define-test month 26 | :parent decoders 27 | (is = 1 (dates:decode-month "ja")) 28 | (is = 2 (dates:decode-month "feb")) 29 | (is = 3 (dates:decode-month "march")) 30 | (is = 4 (dates:decode-month "ap")) 31 | (is = 5 (dates:decode-month "may")) 32 | (is = 6 (dates:decode-month "june")) 33 | (is = 7 (dates:decode-month "jy")) 34 | (is = 8 (dates:decode-month "aug")) 35 | (is = 9 (dates:decode-month "september")) 36 | (is = 10 (dates:decode-month "oct")) 37 | (is = 11 (dates:decode-month "no")) 38 | (is = 12 (dates:decode-month "december"))) 39 | 40 | (define-test unit 41 | :parent decoders 42 | (is = 1/1000 (dates:decode-unit "ms")) 43 | (is = 1 (dates:decode-unit "s")) 44 | (is = 1/10 (dates:decode-unit "decisecond")) 45 | (is = 60 (dates:decode-unit "minute")) 46 | (is = (* 60 60) (dates:decode-unit "h")) 47 | (is = 31536000000000000 (dates:decode-unit "aeons"))) 48 | 49 | (define-test timezone 50 | :parent decoders 51 | (is = 0 (dates:decode-timezone "z")) 52 | (is = 0 (dates:decode-timezone "gmt")) 53 | (is = 0 (dates:decode-timezone "utc")) 54 | (is = 0 (dates:decode-timezone "")) 55 | (is = 9 (dates:decode-timezone "jst"))) 56 | 57 | (define-test integer 58 | :parent decoders 59 | (is = 0 (dates:decode-integer "0")) 60 | (is = 100 (dates:decode-integer "100")) 61 | (is = 0 (dates:decode-integer "zero")) 62 | (is = 40 (dates:decode-integer "fourty")) 63 | (is = 40 (dates:decode-integer "forty")) 64 | (is = 1000 (dates:decode-integer "thousand")) 65 | (is = 11 (dates:decode-integer "eleven")) 66 | (is = 24 (dates:decode-integer "twenty-four")) 67 | (is = 20001 (dates:decode-integer "twenty thousand one")) 68 | (is = 21345 (dates:decode-integer "twenty one thousand three hundred and forty-five"))) 69 | 70 | (define-test parsers 71 | :parent fuzzy-dates 72 | :depends-on (decoders)) 73 | 74 | (define-test forward-time 75 | :parent parsers 76 | (is = (+ 0 10) (dates:parse-forward-time "in 10 seconds" :now 0)) 77 | (is = (+ 0 (* 5 60) 3) (dates:parse-forward-time "in 5 minutes, 3 s" :now 0)) 78 | (is = (+ 0 (* 4 60 60)) (dates:parse-forward-time "in four hours" :now 0)) 79 | (is = (+ 0 (* 42 60 60) 10) (dates:parse-forward-time "in forty two hours ten seconds" :now 0))) 80 | 81 | (define-test backward-time 82 | :parent parsers 83 | (is = (- 0 10) (dates:parse-backward-time "10 seconds ago" :now 0)) 84 | (is = (- 0 (* 30 60 60)) (dates:parse-backward-time "thirty hours ago" :now 0)) 85 | (is = (- 0 3 (* 2 60)) (dates:parse-backward-time "3 s, 2 minutes ago" :now 0))) 86 | 87 | (define-test rfc3339 88 | :parent parsers 89 | (is = (encode-universal-time 15 6 10 16 9 2023 0) (dates:parse-rfc3339-like "2023-09-16T10:06:15.00Z"))) 90 | 91 | (define-test iso8661 92 | :parent parsers 93 | (is = (encode-universal-time 15 6 10 16 9 2023 0) (dates:parse-iso8661-like "20230916T100615Z"))) 94 | 95 | (define-test reverse 96 | :parent parsers 97 | (is = (encode-universal-time 15 6 10 16 9 2023) (dates:parse-reverse-like "10:6:15 16.9.2023"))) 98 | 99 | (define-test rfc1123 100 | :parent parsers 101 | (is = (encode-universal-time 15 6 10 16 9 2023) (dates:parse-rfc1123-like "16 Sep 2023 10:06:15"))) 102 | 103 | (define-test single 104 | :parent parsers 105 | (is = (+ 0 10) (dates:parse-single "10" :now 0))) 106 | 107 | (define-test parse 108 | :parent fuzzy-dates 109 | :depends-on (parsers)) 110 | -------------------------------------------------------------------------------- /documentation.lisp: -------------------------------------------------------------------------------- 1 | (in-package #:org.shirakumo.fuzzy-dates) 2 | 3 | (docs:define-docs 4 | (function decode-weekday 5 | "Turn a weekday name into an integer between 0 and 6. 6 | 7 | If ERRORP is true, failing to parse the month name will signal an 8 | error. Otherwise NIL is returned. 9 | 10 | See http://www.lispworks.com/documentation/HyperSpec/Body/25_ada.htm 11 | 12 | Examples: 13 | 14 | mo 15 | tue 16 | thursday") 17 | 18 | (function encode-weekday 19 | "Turn a weekday identifier [0,6] into its english string name. 20 | 21 | See http://www.lispworks.com/documentation/HyperSpec/Body/25_ada.htm 22 | See DECODE-WEEKDAY") 23 | 24 | (function decode-month 25 | "Turn a month name into an integer between 1 and 12. 26 | 27 | If ERRORP is true, failing to parse the month name will signal an 28 | error. Otherwise NIL is returned. 29 | 30 | See http://www.lispworks.com/documentation/HyperSpec/Body/25_ada.htm 31 | 32 | Examples: 33 | 34 | ja 35 | feb 36 | march") 37 | 38 | (function encode-month 39 | "Turn a month identifier [1,12] into its english string name. 40 | 41 | See http://www.lispworks.com/documentation/HyperSpec/Body/25_ada.htm 42 | See DECODE-MONTH") 43 | 44 | (function decode-unit 45 | "Turn a unit name into a scaling factor relative to seconds. 46 | 47 | If ERRORP is true, failing to parse the month name will signal an 48 | error. Otherwise NIL is returned. 49 | 50 | The supported time units go from nanoseconds to aeons and includes all 51 | sorts of abbreviations for them. 52 | 53 | See http://www.lispworks.com/documentation/HyperSpec/Body/25_ada.htm 54 | 55 | Examples: 56 | 57 | ms 58 | sec 59 | y 60 | aeons") 61 | 62 | (function decode-timezone 63 | "Turn a timezone name into a number of hours of UTC offset. 64 | 65 | If ERRORP is true, failing to parse the month name will signal an 66 | error. Otherwise NIL is returned. 67 | 68 | See http://www.lispworks.com/documentation/HyperSpec/Body/25_ada.htm 69 | 70 | Examples: 71 | 72 | Z 73 | GMT 74 | JST") 75 | 76 | (function decode-integer 77 | "Parse an integer in digit or word format. 78 | 79 | If ERRORP is true, failing to parse the month name will signal an 80 | error. Otherwise NIL is returned. 81 | 82 | The understood words go from one all the way up to trillion, though no 83 | abbreviations are supported. 84 | 85 | Examples: 86 | 87 | 100 88 | one hundred 89 | twenty-two million three hundred two") 90 | 91 | (function parse-forward-time 92 | "Parse a relative time for the future. 93 | 94 | The basic syntax is: 95 | 96 | STAMP ::= in (C U)(,? C U)* 97 | C --- a positive integer 98 | U --- a unit name 99 | 100 | Examples: 101 | 102 | in 10 seconds 103 | in 5 minutes, 10 years 104 | in five hours 105 | 106 | If ERRORP is true, failing to parse the timestring will signal an 107 | error. Otherwise NIL is returned. 108 | 109 | If NOW is given it should be a universal-time timestamp that the 110 | parsed timestring will be relative to. If not given, the current time 111 | is used. 112 | 113 | See DECODE-UNIT 114 | See DECODE-INTEGER 115 | See PARSE") 116 | 117 | (function parse-backward-time 118 | "Parse a relative time for the past. 119 | 120 | The basic syntax is: 121 | 122 | STAMP ::= (C U)(,? C U)* ago 123 | C --- a positive integer 124 | U --- a unit name 125 | 126 | Examples: 127 | 10 seconds ago 128 | 6 years, 5 minutes ago 129 | thirty hours ago 130 | 131 | If ERRORP is true, failing to parse the timestring will signal an 132 | error. Otherwise NIL is returned. 133 | 134 | If NOW is given it should be a universal-time timestamp that the 135 | parsed timestring will be relative to. If not given, the current time 136 | is used. 137 | 138 | See DECODE-UNIT 139 | See DECODE-INTEGER 140 | See PARSE") 141 | 142 | (function parse-date 143 | "Parse a timestamp that only includes a date but not a time. 144 | 145 | A typical date string has the following format: 146 | 147 | 2023.09.16 148 | 149 | This function is more lenient, and makes the following fuzzy matches: 150 | It permits the following separators between date parts: 151 | 152 | space comma period slash dash 153 | 154 | Further, it allows omitting the day part and allows switching the 155 | order around to D M Y rather than the standard Y M D. The american 156 | M D Y format is *not* parsed, as it is ambiguous with Y M D or D M Y 157 | and cannot be distinguished in all cases. 158 | 159 | If ERRORP is true, failing to parse the timestring will signal an 160 | error. Otherwise NIL is returned. 161 | 162 | If NOW is given it should be a universal-time timestamp that the 163 | parsed timestring will be relative to. If not given, the current time 164 | is used. 165 | 166 | See PARSE") 167 | 168 | (function parse-rfc3339-like 169 | "Parse a timestamp that looks vaguely like an RFC3339 date. 170 | 171 | A typical RFC3339 string has the following format: 172 | 173 | 2023-09-16T10:06:15.00-05:00 174 | 175 | This function is more lenient, and makes the following fuzzy matches: 176 | It permits the following separators between date parts: 177 | 178 | space comma period slash dash 179 | 180 | It permits the following separators between date and time parts: 181 | 182 | space dash t 183 | 184 | It permits the following separators between time parts: 185 | 186 | space period dash colon 187 | 188 | It permits date and time parts to not be padded. 189 | 190 | It also permits omitting the date and timezone parts of the 191 | timestamp. 192 | 193 | If ERRORP is true, failing to parse the timestring will signal an 194 | error. Otherwise NIL is returned. 195 | 196 | If NOW is given it should be a universal-time timestamp that the 197 | parsed timestring will be relative to. If not given, the current time 198 | is used. 199 | 200 | See PARSE") 201 | 202 | (function parse-iso8661-like 203 | "Parse a timestamp that looks vaguely like an ISO8661 date. 204 | 205 | This is very similar to the RFC3339 format, but instead follows the 206 | compact format without separators between date and time formats. 207 | 208 | A typical compact ISO8661 string has the following format: 209 | 210 | 20230916T100615Z 211 | 212 | This function is slightly more lenient and permits the following 213 | separators between date and time parts: 214 | 215 | space dash t 216 | 217 | It permits date and time parts to not be padded. 218 | 219 | It also permits omitting the date and timezone parts of the 220 | timestamp. 221 | 222 | If ERRORP is true, failing to parse the timestring will signal an 223 | error. Otherwise NIL is returned. 224 | 225 | If NOW is given it should be a universal-time timestamp that the 226 | parsed timestring will be relative to. If not given, the current time 227 | is used. 228 | 229 | See PARSE") 230 | 231 | (function parse-reverse-like 232 | "Parse a timestamp that is \"reverse\" from the others 233 | 234 | A typical reverse time string has the following format: 235 | 236 | 10:18:03 16.9.2023 237 | 238 | This function is more lenient, and makes the following fuzzy matches: 239 | It permits the following separators between date parts: 240 | 241 | space comma period slash dash 242 | 243 | It permits the following separators between date and time parts: 244 | 245 | space dash t 246 | 247 | It permits the following separators between time parts: 248 | 249 | space period dash colon 250 | 251 | It permits date and time parts to not be padded. 252 | 253 | It also permits omitting the time and timezone parts of the 254 | timestamp. 255 | 256 | If ERRORP is true, failing to parse the timestring will signal an 257 | error. Otherwise NIL is returned. 258 | 259 | If NOW is given it should be a universal-time timestamp that the 260 | parsed timestring will be relative to. If not given, the current time 261 | is used. 262 | 263 | See PARSE") 264 | 265 | (function parse-rfc1123-like 266 | "Parse a timestamp that looks vaguely like an RFC1123 date. 267 | 268 | A typical RFC1123 string has the following format: 269 | 270 | Thu, 23 Jul 2013 19:42:23 GMT 271 | 272 | This function is more lenient, and makes the following fuzzy matches: 273 | It permits the following separators between date parts: 274 | 275 | space comma period slash dash 276 | 277 | It permits the following separators between date and time parts: 278 | 279 | space comma period slash dash t 280 | 281 | It permits the following separators between time parts: 282 | 283 | space period dash colon 284 | 285 | It permits date and time parts to not be padded. 286 | 287 | It also permits omitting the day of week, time, and timezone parts of 288 | the timestamp. 289 | 290 | If ERRORP is true, failing to parse the timestring will signal an 291 | error. Otherwise NIL is returned. 292 | 293 | If NOW is given it should be a universal-time timestamp that the 294 | parsed timestring will be relative to. If not given, the current time 295 | is used. 296 | 297 | See PARSE") 298 | 299 | (function parse-single 300 | "Parse a single token. 301 | 302 | If it's an integer, it can denote either: 303 | 304 | seconds in the future, if below 1000 305 | a year, if below 5000 306 | a UNIX timestamp 307 | 308 | If it's a word, it can denote either: 309 | 310 | a day of the week, pushed to the next week if the current day is 311 | already past. 312 | a month, pushed to the next year if the current month is already 313 | past. 314 | 315 | Examples: 316 | 317 | 10 318 | 1900 319 | mon 320 | march 321 | 322 | If ERRORP is true, failing to parse the timestring will signal an 323 | error. Otherwise NIL is returned. 324 | 325 | If NOW is given it should be a universal-time timestamp that the 326 | parsed timestring will be relative to. If not given, the current time 327 | is used. 328 | 329 | See PARSE") 330 | 331 | (function parse 332 | "Fuzzily parse a time string into a universal-time timestamp. 333 | 334 | If NOW is given it should be a universal-time timestamp that the 335 | parsed timestring will be relative to. If not given, the current time 336 | is used. 337 | 338 | If ERRORP is true, failing to parse the string name will signal an 339 | error. Otherwise NIL is returned. When an error is signalled, two 340 | restarts will be active: 341 | 342 | USE-VALUE -- interactive, allows supplying a universal-time to use 343 | CONTINUE -- simply returns the current universal-time timestamp 344 | 345 | This also applies to all the sub-functions used. 346 | 347 | See PARSE-FORWARD-TIME 348 | See PARSE-BACKWARD-TIME 349 | See PARSE-DATE 350 | See PARSE-RFC3339-LIKE 351 | See PARSE-ISO8661-LIKE 352 | See PARSE-REVERSE-LIKE 353 | See PARSE-RFC1123-LIKE 354 | See PARSE-SINGLE") 355 | 356 | (function print-rfc3339 357 | "Print a universal-time in the RFC3339 format. 358 | 359 | TIME-ZONE may either be a time zone short name string, a time zone 360 | offset as specified by Common Lisp, or NIL for UTC. 361 | 362 | If STREAM is NIL, the result is printed to a string and returned. If 363 | STREAM is T, the result is printed to *STANDARD-OUTPUT*. 364 | 365 | See PRINT 366 | See PARSE-RFC3339-LIKE") 367 | 368 | (function print-iso8661 369 | "Print a universal-time in the ISO8661 format. 370 | 371 | TIME-ZONE may either be a time zone short name string, a time zone 372 | offset as specified by Common Lisp, or NIL for UTC. 373 | 374 | If STREAM is NIL, the result is printed to a string and returned. If 375 | STREAM is T, the result is printed to *STANDARD-OUTPUT*. 376 | 377 | See PRINT 378 | See PARSE-ISO8661-LIKE") 379 | (function print-reverse 380 | "Print a universal-time in a format that is \"reverse\" from the others. 381 | 382 | TIME-ZONE may either be a time zone short name string, a time zone 383 | offset as specified by Common Lisp, or NIL for UTC. 384 | 385 | If STREAM is NIL, the result is printed to a string and returned. If 386 | STREAM is T, the result is printed to *STANDARD-OUTPUT*. 387 | 388 | See PRINT 389 | See PARSE-REVERSE-LIKE") 390 | 391 | (function print-rfc1123 392 | "Print a universal-time in the RFC1123 format. 393 | 394 | TIME-ZONE may either be a time zone short name string, a time zone 395 | offset as specified by Common Lisp, or NIL for UTC. 396 | 397 | If STREAM is NIL, the result is printed to a string and returned. If 398 | STREAM is T, the result is printed to *STANDARD-OUTPUT*. 399 | 400 | See PRINT 401 | See PARSE-RFC1123-LIKE") 402 | 403 | (function print-relative 404 | "Print a universal-time as a relative amount of time. 405 | 406 | If NOW is given it should be a universal-time timestamp that the 407 | printed timestring will be relative to. If not given, the current time 408 | is used. 409 | 410 | If STREAM is NIL, the result is printed to a string and returned. If 411 | STREAM is T, the result is printed to *STANDARD-OUTPUT*. 412 | 413 | See PRINT 414 | See PARSE-FORWARD-TIME 415 | See PARSE-BACKWARD-TIME") 416 | 417 | (function print-date 418 | "Print a universal-time as a YYY.MM.DD date. 419 | 420 | TIME-ZONE may either be a time zone short name string, a time zone 421 | offset as specified by Common Lisp, or NIL for UTC. 422 | 423 | If STREAM is NIL, the result is printed to a string and returned. If 424 | STREAM is T, the result is printed to *STANDARD-OUTPUT*. 425 | 426 | See PRINT 427 | See PARSE-SINGLE") 428 | 429 | (function print-clock 430 | "Print a universal-time as a HH:MM:SS time. 431 | 432 | TIME-ZONE may either be a time zone short name string, a time zone 433 | offset as specified by Common Lisp, or NIL for UTC. 434 | 435 | If STREAM is NIL, the result is printed to a string and returned. If 436 | STREAM is T, the result is printed to *STANDARD-OUTPUT*. 437 | 438 | See PRINT 439 | See PARSE-SINGLE") 440 | 441 | (function print 442 | "Print a universal-time stamp into a print representation. 443 | 444 | FORMAT may be one of the following: 445 | 446 | :RFC3339 -- PRINT-RFC3339 447 | :ISO8661 -- PRINT-ISO8661 448 | :REVERSE -- PRINT-REVERSE 449 | :RFC1123 -- PRINT-RFC1123 450 | :RELATIVE -- PRINT-RELATIVE 451 | :DATE -- PRINT-DATE 452 | :CLOCK -- PRINT-CLOCK 453 | 454 | The STREAM and TIME-ZONE are passed along. NOW is only used for the 455 | :RELATIVE format. 456 | 457 | See PRINT-RFC3339 458 | See PRINT-ISO8661 459 | See PRINT-REVERSE 460 | See PRINT-RFC1123 461 | See PRINT-RELATIVE 462 | See PRINT-DATE 463 | See PRINT-CLOCK")) 464 | -------------------------------------------------------------------------------- /fuzzy-dates.lisp: -------------------------------------------------------------------------------- 1 | (in-package #:org.shirakumo.fuzzy-dates) 2 | 3 | (defun parse-integer* (thing) 4 | (when (and thing (string/= "" thing)) 5 | (parse-integer thing))) 6 | 7 | (defmacro with-integers-bound (vars (regex string) &body body) 8 | `(cl-ppcre:register-groups-bind ,vars (,(format NIL regex) ,string) 9 | (let ,(loop for var in vars 10 | unless (char= #\_ (char (string var) 0)) 11 | collect `(,var (parse-integer* ,var))) 12 | ,@body))) 13 | 14 | (defmacro with-scans (var &body cases) 15 | (let ((varg (gensym "VAR"))) 16 | `(let ((,varg ,var)) 17 | (cond ,@(loop for (regex . body) in cases 18 | collect (if (member regex '(T :otherwise otherwise)) 19 | `(T ,@body) 20 | `((cl-ppcre:scan ,(format NIL "^~a$" regex) ,varg) ,@body))))))) 21 | 22 | (defun check-error (errorp string &optional (default (get-universal-time))) 23 | (when errorp 24 | (restart-case 25 | (error "Unknown date string: ~a" string) 26 | (use-value (v) 27 | :report "Supply a universal time" 28 | :interactive (lambda () 29 | (format *query-io* "~&Enter a universal time: ") 30 | (parse-integer (read-line *query-io*))) 31 | :test integerp 32 | v) 33 | (continue () 34 | :report "Return the current time" 35 | default)))) 36 | 37 | (defun backfill-timestamp (y o d h m s tz &optional (now (get-universal-time))) 38 | (multiple-value-bind (ls lm lh ld lo ly) (apply #'decode-universal-time now (if tz (list tz))) 39 | (let ((s (or s ls)) 40 | (m (or m lm)) 41 | (h (or h lh)) 42 | (d (or d ld)) 43 | (o (or o lo)) 44 | (y (or y ly)) 45 | (tz (if tz (list tz)))) 46 | (when (and (<= 1 o 12) (<= 1 d 32) (<= 0 h 23) (<= 0 m 59) (<= 0 s 59)) 47 | (let ((time (apply #'encode-universal-time s m h d o y tz))) 48 | (when (and y (< 100 y)) 49 | (incf y 1900)) 50 | time))))) 51 | 52 | (eval-when (:compile-toplevel :load-toplevel :execute) 53 | (defun parse-tzdb (db) 54 | (with-open-file (stream db) 55 | (let ((db (make-hash-table :test 'equalp))) 56 | (loop for line = (read-line stream NIL) 57 | while line 58 | unless (and (<= (length line) 1) (char= (char line 0) #\#)) 59 | collect (with-integers-bound (_z _+ h m) ("(\\w+) *([+-])?(\\d+)(?::(\\d+))?" line) 60 | (setf (gethash (string-downcase _z) db) 61 | (- (* (if (string= _+ "-") -1 +1) (+ h (/ (or m 0) 60))))))) 62 | db)))) 63 | 64 | (defparameter *tzdb* (parse-tzdb #.(make-pathname :name "tz" :type "txt" :defaults (or *compile-file-pathname* *load-pathname*)))) 65 | 66 | (defun decode-weekday (w &optional errorp) 67 | (with-scans w 68 | ("(mo|mon|monday)" 0) 69 | ("(tu|tue|tuesday)" 1) 70 | ("(we|wed|wednesday)" 2) 71 | ("(th|thu|thursday)" 3) 72 | ("(fr|fri|friday)" 4) 73 | ("(sa|sat|saturday)" 5) 74 | ("(su|sun|sunday)" 6) 75 | (T (when errorp (error "Unknown weekday: ~a" w))))) 76 | 77 | (defun encode-weekday (w &key length) 78 | (let ((name (ecase w 79 | (0 "monday") 80 | (1 "tuesday") 81 | (2 "wednesday") 82 | (3 "thursday") 83 | (4 "friday") 84 | (5 "saturday") 85 | (6 "sunday")))) 86 | (if length 87 | (subseq name 0 length) 88 | name))) 89 | 90 | (defun decode-month (m &optional errorp) 91 | (with-scans m 92 | ("(ja|jan|january)" 1) 93 | ("(fe|feb|february)" 2) 94 | ("(ma|mar|march)" 3) 95 | ("(ap|apr|april)" 4) 96 | ("(my|may)" 5) 97 | ("(ju|jun|june)" 6) 98 | ("(jy|jul|july)" 7) 99 | ("(au|aug|august)" 8) 100 | ("(se|sep|september)" 9) 101 | ("(oc|oct|october)" 10) 102 | ("(no|nov|november)" 11) 103 | ("(de|dec|december)" 12) 104 | (T (when errorp (error "Unknown month: ~a" m))))) 105 | 106 | (defun encode-month (m &key length) 107 | (let ((name (ecase m 108 | (1 "january") 109 | (2 "february") 110 | (3 "march") 111 | (4 "april") 112 | (5 "may") 113 | (6 "june") 114 | (7 "july") 115 | (8 "august") 116 | (9 "september") 117 | (10 "october") 118 | (11 "november") 119 | (12 "december")))) 120 | (if length 121 | (subseq name 0 length) 122 | name))) 123 | 124 | (defun decode-unit (u &optional errorp) 125 | (with-scans u 126 | ("(ns|nano|nanoseconds?)" 1/1000000000) 127 | ("(us|micro|microseconds?)" 1/1000000) 128 | ("(ms|milli|milliseconds?)" 1/1000) 129 | ("(cs|centi|centiseconds?)" 1/100) 130 | ("(ds|deci|deciseconds?)" 1/10) 131 | ("(s|sec|secs|seconds?)?" 1) 132 | ("(m|min|minutes?)" 60) 133 | ("(h|hours?)" 3600) 134 | ("(d|days?)" 86400) 135 | ("(w|weeks?)" 604800) 136 | ("(o|mo|months?)" 2592000) 137 | ("(y|years?)" 31536000) 138 | ("(dc|dec|decades?)" 315532800) 139 | ("(c|cen|century|centuries)" 3155695200) 140 | ("(a|aeons?)" 31536000000000000) 141 | (T (when errorp (error "Unknown time unit: ~a" u))))) 142 | 143 | (defun decode-integer (i &optional errorp) 144 | (with-scans i 145 | (".*[ -].*" 146 | (let ((total 0) (accum 0) (prev most-positive-fixnum)) 147 | (loop for part in (cl-ppcre:split "[ -]+(and[ -]+)?" i) 148 | for int = (or (decode-integer part errorp) 149 | (return-from decode-integer NIL)) 150 | do (if (< prev int) 151 | (progn (incf total (* int accum)) 152 | (setf accum 0)) 153 | (incf accum int)) 154 | (setf prev int)) 155 | (+ total accum))) 156 | ("\\d+" (parse-integer i)) 157 | ("zero|nil|none" 0) 158 | ("one" 1) 159 | ("two" 2) 160 | ("three" 3) 161 | ("four" 4) 162 | ("five" 5) 163 | ("six" 6) 164 | ("seven" 7) 165 | ("eight" 8) 166 | ("nine" 9) 167 | ("ten" 10) 168 | ("eleven" 11) 169 | ("twelve" 12) 170 | ("thirteen" 13) 171 | ("fourteen" 14) 172 | ("fifteen" 15) 173 | ("sixteen" 16) 174 | ("seventeen" 17) 175 | ("eighteen" 18) 176 | ("nineteen" 19) 177 | ("twenty" 20) 178 | ("thirty" 30) 179 | ("fou?rty" 40) 180 | ("fifty" 50) 181 | ("sixty" 60) 182 | ("seventy" 70) 183 | ("eighty" 80) 184 | ("ninety" 90) 185 | ("hundred" 100) 186 | ("thousand" 1000) 187 | ("million" 1000000) 188 | ("billion" 1000000000) 189 | ("trillion" 1000000000000) 190 | (T (when errorp (error "Unknown integer: ~a" i))))) 191 | 192 | (defun decode-timezone (tz &optional errorp) 193 | (or (when (or (null tz) (string= "z" tz) (string= "" tz)) 0) 194 | (gethash tz *tzdb*) 195 | (when errorp (error "Unknown time zone: ~a" tz)))) 196 | 197 | (defun encode-timezone (tz &optional stream) 198 | (cond ((stringp tz) (write-string tz stream)) 199 | ((or (null tz) (= 0 tz)) (format stream "Z")) 200 | (T (format stream "+~2,'0d~[~:;~:*:~2,'0d~]" (truncate tz) (mod (* tz 60) 60))))) 201 | 202 | (defmacro define-parser (name (strvar &optional nowvar (errorp (gensym "ERRORP"))) &body body) 203 | `(defun ,name (string &key ,@(when nowvar '(now)) errorp) 204 | (let ((,strvar (string-downcase string)) 205 | (,errorp errorp) 206 | ,@(when nowvar `((,nowvar (or now (get-universal-time)))))) 207 | (or ,@body 208 | (check-error ,errorp ,strvar ,(or nowvar '(get-universal-time))))))) 209 | 210 | (define-parser parse-timezone (string) 211 | ;; JST+5:00 212 | (or (with-integers-bound (_z _+ oh om) ("([A-Za-z]+)(?: *([+\\-])(\\d{1,2})(?::*(\\d+))?)?$" string) 213 | (+ (decode-timezone (string-downcase _z)) 214 | (* (if (string= "-" _+) -1 +1) 215 | (+ (* (or oh 0) (/ (or om 0) 60)))))) 216 | (with-integers-bound (_+ oh om) ("([+\\-])(\\d{1,2})(?::*(\\d+))?$" string) 217 | (* (if (string= "-" _+) -1 +1) 218 | (+ (* (or oh 0) (/ (or om 0) 60))))))) 219 | 220 | (defun parse-relative-time (string errorp) 221 | (let ((sum 0) 222 | (accum ())) 223 | ;; We loop until we find a unit and then use all preceding tokens as an integer 224 | (dolist (part (cl-ppcre:split "[ ,]+(and[ ,]+)?" string)) 225 | (let ((u (decode-unit part))) 226 | (cond (u 227 | (incf sum (* u 228 | (or (decode-integer (format NIL "~{~a~^ ~}" (nreverse accum)) errorp) 229 | (return NIL)))) 230 | (setf accum ())) 231 | (T 232 | (push part accum))))) 233 | ;; If we have non-unit tokens leftover, we fail to parse. 234 | (if accum 235 | (check-error errorp string) 236 | sum))) 237 | 238 | (define-parser parse-forward-time (string now errorp) 239 | ;; in 5m, 9s 240 | (cl-ppcre:register-groups-bind (parts) ("^in *(.*)$" string) 241 | (let ((offset (parse-relative-time parts errorp))) 242 | (when offset (+ now offset))))) 243 | 244 | (define-parser parse-backward-time (string now errorp) 245 | ;; 10 seconds ago 246 | (cl-ppcre:register-groups-bind (parts) ("^(.*) *ago$" string) 247 | (let ((offset (parse-relative-time parts errorp))) 248 | (when offset (- now offset))))) 249 | 250 | (define-parser parse-date (string now) 251 | ;; 2023/09/15 252 | ;; 9.2023 253 | (with-integers-bound (y o d) ("^(\\d+)[ ,./\\-](\\d+)(?:[ ,./\\-](\\d+))?$" string) 254 | (if d 255 | (when (< 31 d) (rotatef y d)) 256 | (when (< 12 o) (rotatef y o))) 257 | (backfill-timestamp y o d NIL NIL NIL NIL now))) 258 | 259 | (define-parser parse-rfc3339-like (string now) 260 | ;; 2023.09.15T20:35:42Z 261 | (with-integers-bound (y o d h m s) ("^(?:(\\d+)[ ,./\\-](\\d+)(?:[ ,./\\-](\\d+))?[t\\- ]+)?~ 262 | (\\d+)[ .:\\-]+(\\d+)(?:[ .:\\-]+(\\d+))?(?:\\.+\\d*)?" string) 263 | (backfill-timestamp y o d h m s (parse-timezone string) now))) 264 | 265 | (define-parser parse-iso8661-like (string now) 266 | ;; 20230915T203542Z 267 | (with-integers-bound (y o d h m s) ("^(?:(\\d{4})?(\\d{2})(\\d{2})[t\\- ]+)?~ 268 | (\\d{2})(\\d{2})(\\d{2})?" string) 269 | (backfill-timestamp y o d h m s (parse-timezone string) now))) 270 | 271 | (define-parser parse-reverse-like (string now) 272 | ;; 22:48:34 15.9.2023 GMT 273 | (with-integers-bound (h m s d o y) ("^(?:(\\d+)[ .:\\-](\\d+)(?:[ .:\\-](\\d+))?[t\\- ]+)?~ 274 | (\\d+)[ ,./\\-](\\d+)(?:[ ,./\\-](\\d+))?" string) 275 | (backfill-timestamp y o d h m s (parse-timezone string) now))) 276 | 277 | (define-parser parse-rfc1123-like (string now) 278 | ;; Thu, 23 Jul 2013 19:42:23 GMT 279 | (with-integers-bound (_dow d _o y h m s) ("^([A-Za-z]+)[ ,./\\-]*(\\d+)[ ,./\\-]*([A-Za-z]+)[ ,./\\-]*(\\d+)~ 280 | (?:[t ,./\\-]*(\\d+)[ .:\\-]+(\\d+)(?:[ .:\\-]+(\\d+))?(?:\\.+\\d*)?)?" string) 281 | (backfill-timestamp y (decode-month _o T) d h m s (parse-timezone string) now)) 282 | (with-integers-bound (d _o y h m s) ("^(\\d+)[ ,./\\-]*([A-Za-z]+)[ ,./\\-]*(\\d+)~ 283 | (?:[t ,./\\-]*(\\d+)[ .:\\-]+(\\d+)(?:[ .:\\-]+(\\d+))?(?:\\.+\\d*)?)?" string) 284 | (backfill-timestamp y (decode-month _o T) d h m s (parse-timezone string) now))) 285 | 286 | (define-parser parse-single (string now) 287 | (with-scans string 288 | ("\\d+(\\.\\d+)" 289 | (let ((stamp (parse-integer string :junk-allowed T))) 290 | (cond ((< stamp 1000) ;; Seconds in the future 291 | (+ stamp now)) 292 | ((< stamp 5000) ;; A year relative to current time 293 | (multiple-value-bind (ls lm lh ld lo) (decode-universal-time now) 294 | (encode-universal-time ls lm lh ld lo stamp))) 295 | (T ;; A UNIX or Universal timestamp 296 | (let ((diff (abs (- stamp now)))) 297 | (if (< diff (encode-universal-time 0 0 0 1 1 1970 NIL)) 298 | stamp 299 | (+ stamp (encode-universal-time 0 0 0 1 1 1970 NIL)))))))) 300 | ("(just +)?now" 301 | now) 302 | ("[A-Za-z]+" 303 | (let ((w (decode-weekday string)) 304 | (o (decode-month string))) 305 | (multiple-value-bind (ls lm lh ld lo ly lw) (decode-universal-time now) 306 | (cond (w ;; A day of the week relative to current time, always in the future 307 | (+ now (* (decode-unit "d") (if (< lw w) (- w lw) (- 7 (- lw w)))))) 308 | (o ;; A month relative to current time, always in the future 309 | (encode-universal-time ls lm lh ld o (if (< lo o) ly (1+ ly)))))))))) 310 | 311 | (defun parse (string &key now errorp) 312 | (let ((now (or now (get-universal-time)))) 313 | (or (parse-forward-time string :now now) 314 | (parse-backward-time string :now now) 315 | (parse-date string :now now) 316 | (parse-rfc3339-like string :now now) 317 | (parse-iso8661-like string :now now) 318 | (parse-reverse-like string :now now) 319 | (parse-rfc1123-like string :now now) 320 | (parse-single string :now now) 321 | (check-error errorp string now)))) 322 | 323 | (defmacro define-printer (name (stream &optional ss mm hh d m y dow dsp tz) &body body) 324 | (let ((binds (loop for var in (list ss mm hh d m y dow dsp tz) 325 | collect (or var (gensym))))) 326 | `(defun ,name (stamp &optional stream time-zone) 327 | (flet ((thunk (,stream) 328 | (multiple-value-bind ,binds (decode-universal-time stamp (etypecase time-zone 329 | (null NIL) 330 | (string (gethash time-zone *tzdb*)) 331 | (real time-zone))) 332 | (declare (ignore ,@(loop for symb in binds 333 | unless (symbol-package symb) 334 | collect symb))) 335 | ,@body))) 336 | (etypecase stream 337 | (null 338 | (with-output-to-string (stream) 339 | (thunk stream))) 340 | ((eql T) 341 | (thunk *standard-output*)) 342 | (stream 343 | (thunk stream))))))) 344 | 345 | (define-printer print-rfc3339 (stream ss mm hh d m y) 346 | (format stream "~4,'0d.~2,'0d.~2,'0dT~2,'0d:~2,'0d:~2,'0d" 347 | y m d hh mm ss) 348 | (encode-timezone time-zone stream)) 349 | 350 | (define-printer print-iso8661 (stream ss mm hh d m y) 351 | (format stream "~4,'0d~2,'0d~2,'0dT~2,'0d~2,'0d~2,'0d" 352 | y m d hh mm ss) 353 | (encode-timezone time-zone stream)) 354 | 355 | (define-printer print-reverse (stream ss mm hh d m y) 356 | (format stream "~2,'0:d~2,'0:d~2,'0d ~2,'0d.~2,'0d.~4,'0d " 357 | hh mm ss d m y) 358 | (encode-timezone time-zone stream)) 359 | 360 | (define-printer print-rfc1123 (stream ss mm hh d m y dow) 361 | (format stream "~a, ~d ~a ~d ~d:~2,'0d:~2,'0d " 362 | (encode-weekday dow :length 3) d 363 | (encode-month m :length 3) y hh mm ss) 364 | (encode-timezone time-zone stream)) 365 | 366 | (define-printer print-date (stream NIL NIL NIL d m y) 367 | (format stream "~4,'0d.~2,'0d.~2,'0d" 368 | y m d)) 369 | 370 | (define-printer print-clock (stream ss mm hh) 371 | (format stream "~d:~2,'0d:~2,'0d" 372 | hh mm ss)) 373 | 374 | (defun print-relative (stamp &optional stream now) 375 | (let ((diff (- stamp (or now (get-universal-time))))) 376 | (labels ((format-relative (stream) 377 | (let ((seconds (mod (floor (/ diff 1)) 60)) 378 | (minutes (mod (floor (/ diff 60)) 60)) 379 | (hours (mod (floor (/ diff 60 60)) 24)) 380 | (days (mod (floor (/ diff 60 60 24)) 7)) 381 | ;; We approximate by saying each month has four weeks 382 | (weeks (mod (floor (/ diff 60 60 24 7)) 4)) 383 | (months (mod (floor (/ diff 60 60 24 7 4)) 12)) 384 | ;; More accurate through diff in a year 385 | (years (mod (floor (/ diff 31557600)) 10)) 386 | (decades (mod (floor (/ diff 31557600 10)) 10)) 387 | (centuries (mod (floor (/ diff 31557600 10 10)) (expt 10 (- 9 2)))) 388 | (aeons (floor (/ diff 31557600 10 10 (expt 10 (- 9 2))))) 389 | (non-NIL ())) 390 | (flet ((p (i format) (when (< 0 i) (push (format NIL format i) non-NIL)))) 391 | (p seconds "~a second~:p") 392 | (p minutes "~a minute~:p") 393 | (p hours "~a hour~:p") 394 | (p days "~a day~:p") 395 | (p weeks "~a week~:p") 396 | (p months "~a month~:p") 397 | (p years "~a year~:p") 398 | (p decades "~a decade~:p") 399 | (p centuries "~a centur~:@p") 400 | (p aeons "~a aeon~:p") 401 | (format stream "~{~a~^, ~}" non-NIL)))) 402 | (thunk (stream) 403 | (cond ((= 0 diff) 404 | (format stream "now")) 405 | ((< 0 diff) 406 | (format stream "in ") 407 | (format-relative stream)) 408 | (T 409 | (format-relative stream) 410 | (format stream " ago"))))) 411 | (etypecase stream 412 | (null 413 | (with-output-to-string (stream) 414 | (thunk stream))) 415 | ((eql T) 416 | (thunk *standard-output*)) 417 | (stream 418 | (thunk stream)))))) 419 | 420 | (defun print (stamp &key (format :rfc3339) stream time-zone now) 421 | (ecase format 422 | (:rfc3339 (print-rfc3339 stamp stream time-zone)) 423 | (:iso8661 (print-iso8661 stamp stream time-zone)) 424 | (:reverse (print-reverse stamp stream time-zone)) 425 | (:rfc1123 (print-rfc1123 stamp stream time-zone)) 426 | (:relative (print-relative stamp stream now)) 427 | (:date (print-date stamp stream time-zone)) 428 | (:clock (print-clock stamp stream time-zone)))) 429 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | Fuzzy Dates

fuzzy dates

1.0.0

A library to fuzzily parse date strings

About Fuzzy-Dates

This is a small library to very fuzzily parse time and date strings into a universal-time timestamp. Among the fuzzily supported formats are:

  • Forwards and backwards relative stamps

  • Clocks

  • RFC1123

  • RFC3339

  • ISO8661

  • Weekdays and months

  • UNIX timestamps

In short, it should parse a lot of what you throw at it and give you a reasonable timestamp for it. Note that while this does make this parsing rather suitable for parsing human input, there's still ambiguities that cannot be resolved, such as whether the user thinks dates are YMD, DMY, or MDY, and the human mind's ingenuity in coming up with new ways to write dates and times can't all be accounted for, so there are definitely timestamps that we can understand, which this library won't be able to parse or will parse different to how we understand it.

System Information

1.0.0
Yukari Hafner
zlib

Definition Index

  • ORG.SHIRAKUMO.FUZZY-DATES

      No documentation provided.
      • EXTERNAL FUNCTION

        DECODE-INTEGER

          • I
          • &OPTIONAL
          • ERRORP
          Source
          Parse an integer in digit or word format.
            2 | 
            3 | If ERRORP is true, failing to parse the month name will signal an
            4 | error. Otherwise NIL is returned.
            5 | 
            6 | The understood words go from one all the way up to trillion, though no
            7 | abbreviations are supported.
            8 | 
            9 | Examples:
           10 | 
           11 |   100
           12 |   one hundred
           13 |   twenty-two million three hundred two
        • EXTERNAL FUNCTION

          DECODE-UNIT

            • U
            • &OPTIONAL
            • ERRORP
            Source
            Turn a unit name into a scaling factor relative to seconds.
             36 | 
             37 | If ERRORP is true, failing to parse the month name will signal an
             38 | error. Otherwise NIL is returned.
             39 | 
             40 | The supported time units go from nanoseconds to aeons and includes all
             41 | sorts of abbreviations for them.
             42 | 
             43 | See http://www.lispworks.com/documentation/HyperSpec/Body/25_ada.htm
             44 | 
             45 | Examples:
             46 | 
             47 |   ms
             48 |   sec
             49 |   y
             50 |   aeons
          • EXTERNAL FUNCTION

            PARSE

              • STRING
              • &KEY
              • NOW
              • ERRORP
              Source
              Fuzzily parse a time string into a universal-time timestamp.
               62 | 
               63 | If NOW is given it should be a universal-time timestamp that the
               64 | parsed timestring will be relative to. If not given, the current time
               65 | is used.
               66 | 
               67 | If ERRORP is true, failing to parse the string name will signal an
               68 | error. Otherwise NIL is returned. When an error is signalled, two
               69 | restarts will be active:
               70 | 
               71 |   USE-VALUE -- interactive, allows supplying a universal-time to use
               72 |   CONTINUE  -- simply returns the current universal-time timestamp
               73 | 
               74 | This also applies to all the sub-functions used.
               75 | 
               76 | See PARSE-FORWARD-TIME
               77 | See PARSE-BACKWARD-TIME
               78 | See PARSE-RFC3339-LIKE
               79 | See PARSE-ISO8661-LIKE
               80 | See PARSE-REVERSE-LIKE
               81 | See PARSE-RFC1123-LIKE
               82 | See PARSE-SINGLE
            • EXTERNAL FUNCTION

              PARSE-BACKWARD-TIME

                • STRING
                • &KEY
                • NOW
                • ERRORP
                Source
                Parse a relative time for the past.
                 83 | 
                 84 | The basic syntax is:
                 85 | 
                 86 |   STAMP ::= (C U)(,? C U)* ago
                 87 |   C     --- a positive integer
                 88 |   U     --- a unit name
                 89 | 
                 90 | Examples:
                 91 |   10 seconds ago
                 92 |   6 years, 5 minutes ago
                 93 |   thirty hours ago
                 94 | 
                 95 | If ERRORP is true, failing to parse the timestring will signal an
                 96 | error. Otherwise NIL is returned.
                 97 | 
                 98 | If NOW is given it should be a universal-time timestamp that the
                 99 | parsed timestring will be relative to. If not given, the current time
                100 | is used.
                101 | 
                102 | See DECODE-UNIT
                103 | See DECODE-INTEGER
                104 | See PARSE
              • EXTERNAL FUNCTION

                PARSE-FORWARD-TIME

                  • STRING
                  • &KEY
                  • NOW
                  • ERRORP
                  Source
                  Parse a relative time for the future.
                  105 | 
                  106 | The basic syntax is:
                  107 | 
                  108 |   STAMP ::= in (C U)(,? C U)*
                  109 |   C     --- a positive integer
                  110 |   U     --- a unit name
                  111 | 
                  112 | Examples:
                  113 | 
                  114 |   in 10 seconds
                  115 |   in 5 minutes, 10 years
                  116 |   in five hours
                  117 | 
                  118 | If ERRORP is true, failing to parse the timestring will signal an
                  119 | error. Otherwise NIL is returned.
                  120 | 
                  121 | If NOW is given it should be a universal-time timestamp that the
                  122 | parsed timestring will be relative to. If not given, the current time
                  123 | is used.
                  124 | 
                  125 | See DECODE-UNIT
                  126 | See DECODE-INTEGER
                  127 | See PARSE
                • EXTERNAL FUNCTION

                  PARSE-ISO8661-LIKE

                    • STRING
                    • &KEY
                    • NOW
                    • ERRORP
                    Source
                    Parse a timestamp that looks vaguely like an ISO8661 date.
                    128 | 
                    129 | This is very similar to the RFC3339 format, but instead follows the
                    130 | compact format without separators between date and time formats.
                    131 | 
                    132 | A typical compact ISO8661 string has the following format:
                    133 | 
                    134 |   20230916T100615Z
                    135 | 
                    136 | This function is slightly more lenient and permits the following
                    137 | separators between date and time parts:
                    138 | 
                    139 |   space dash t
                    140 | 
                    141 | It permits date and time parts to not be padded.
                    142 | 
                    143 | It also permits omitting the date and timezone parts of the
                    144 | timestamp.
                    145 | 
                    146 | If ERRORP is true, failing to parse the timestring will signal an
                    147 | error. Otherwise NIL is returned.
                    148 | 
                    149 | If NOW is given it should be a universal-time timestamp that the
                    150 | parsed timestring will be relative to. If not given, the current time
                    151 | is used.
                    152 | 
                    153 | See PARSE
                  • EXTERNAL FUNCTION

                    PARSE-REVERSE-LIKE

                      • STRING
                      • &KEY
                      • NOW
                      • ERRORP
                      Source
                      Parse a timestamp that is "reverse" from the others
                      154 | 
                      155 | A typical reverse time string has the following format:
                      156 | 
                      157 |   10:18:03 16.9.2023
                      158 | 
                      159 | This function is more lenient, and makes the following fuzzy matches:
                      160 | It permits the following separators between date parts:
                      161 | 
                      162 |   space comma period slash dash
                      163 | 
                      164 | It permits the following separators between date and time parts:
                      165 | 
                      166 |   space dash t
                      167 | 
                      168 | It permits the following separators between time parts:
                      169 | 
                      170 |   space period dash colon
                      171 | 
                      172 | It permits date and time parts to not be padded.
                      173 | 
                      174 | It also permits omitting the time and timezone parts of the
                      175 | timestamp.
                      176 | 
                      177 | If ERRORP is true, failing to parse the timestring will signal an
                      178 | error. Otherwise NIL is returned.
                      179 | 
                      180 | If NOW is given it should be a universal-time timestamp that the
                      181 | parsed timestring will be relative to. If not given, the current time
                      182 | is used.
                      183 | 
                      184 | See PARSE
                    • EXTERNAL FUNCTION

                      PARSE-RFC1123-LIKE

                        • STRING
                        • &KEY
                        • NOW
                        • ERRORP
                        Source
                        Parse a timestamp that looks vaguely like an RFC1123 date.
                        185 | 
                        186 | A typical RFC1123 string has the following format:
                        187 | 
                        188 |   Thu, 23 Jul 2013 19:42:23 GMT
                        189 | 
                        190 | This function is more lenient, and makes the following fuzzy matches:
                        191 | It permits the following separators between date parts:
                        192 | 
                        193 |   space comma period slash dash
                        194 | 
                        195 | It permits the following separators between date and time parts:
                        196 | 
                        197 |   space comma period slash dash t
                        198 | 
                        199 | It permits the following separators between time parts:
                        200 | 
                        201 |   space period dash colon
                        202 | 
                        203 | It permits date and time parts to not be padded.
                        204 | 
                        205 | It also permits omitting the day of week, time, and timezone parts of
                        206 | the timestamp.
                        207 | 
                        208 | If ERRORP is true, failing to parse the timestring will signal an
                        209 | error. Otherwise NIL is returned.
                        210 | 
                        211 | If NOW is given it should be a universal-time timestamp that the
                        212 | parsed timestring will be relative to. If not given, the current time
                        213 | is used.
                        214 | 
                        215 | See PARSE
                      • EXTERNAL FUNCTION

                        PARSE-RFC3339-LIKE

                          • STRING
                          • &KEY
                          • NOW
                          • ERRORP
                          Source
                          Parse a timestamp that looks vaguely like an RFC3339 date.
                          216 | 
                          217 | A typical RFC3339 string has the following format:
                          218 | 
                          219 |   2023-09-16T10:06:15.00-05:00
                          220 | 
                          221 | This function is more lenient, and makes the following fuzzy matches:
                          222 | It permits the following separators between date parts:
                          223 | 
                          224 |   space comma period slash dash
                          225 | 
                          226 | It permits the following separators between date and time parts:
                          227 | 
                          228 |   space dash t
                          229 | 
                          230 | It permits the following separators between time parts:
                          231 | 
                          232 |   space period dash colon
                          233 | 
                          234 | It permits date and time parts to not be padded.
                          235 | 
                          236 | It also permits omitting the date and timezone parts of the
                          237 | timestamp.
                          238 | 
                          239 | If ERRORP is true, failing to parse the timestring will signal an
                          240 | error. Otherwise NIL is returned.
                          241 | 
                          242 | If NOW is given it should be a universal-time timestamp that the
                          243 | parsed timestring will be relative to. If not given, the current time
                          244 | is used.
                          245 | 
                          246 | See PARSE
                        • EXTERNAL FUNCTION

                          PARSE-SINGLE

                            • STRING
                            • &KEY
                            • NOW
                            • ERRORP
                            Source
                            Parse a single token.
                            247 | 
                            248 | If it's an integer, it can denote either:
                            249 | 
                            250 |   seconds in the future, if below 1000
                            251 |   a year, if below 5000
                            252 |   a UNIX timestamp
                            253 | 
                            254 | If it's a word, it can denote either:
                            255 | 
                            256 |   a day of the week, pushed to the next week if the current day is
                            257 |   already past.
                            258 |   a month, pushed to the next year if the current month is already
                            259 |   past.
                            260 | 
                            261 | Examples:
                            262 | 
                            263 |   10
                            264 |   1900
                            265 |   mon
                            266 |   march
                            267 | 
                            268 | If ERRORP is true, failing to parse the timestring will signal an
                            269 | error. Otherwise NIL is returned.
                            270 | 
                            271 | If NOW is given it should be a universal-time timestamp that the
                            272 | parsed timestring will be relative to. If not given, the current time
                            273 | is used.
                            274 | 
                            275 | See PARSE
                        --------------------------------------------------------------------------------