├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── bnf.txt ├── distribute ├── dte ├── __init__.py └── dte ├── requirements.txt ├── setup.py └── test ├── __init__.py └── test.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | */**/parser.out 3 | */**/parsetab.py 4 | dte.egg-info/* 5 | dist/* 6 | init 7 | .vim 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - '3.6' 4 | - '3.7' 5 | - '3.8' 6 | - '3.9' 7 | os: 8 | - linux 9 | before_script: 10 | - chmod +x dte/dte 11 | install: 12 | - pip install -r requirements.txt 13 | script: 14 | - pytest test/test.py && echo Tests passed. 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 2 | Version 2, December 2004 3 | 4 | Copyright (C) 2004 Sam Hocevar 5 | 6 | Everyone is permitted to copy and distribute verbatim or modified 7 | copies of this license document, and changing it is allowed as long 8 | as the name is changed. 9 | 10 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 11 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 12 | 13 | 0. You just DO WHAT THE FUCK YOU WANT TO. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Date Time Expression 2 | 3 | ![PyPI](https://img.shields.io/pypi/v/dte) 4 | ![Travis (.com)](https://img.shields.io/travis/com/mvrozanti/dte) 5 | ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/dte) 6 | [![License: WTFPL](https://img.shields.io/badge/License-WTFPL-orange.svg)](http://www.wtfpl.net/about/) 7 | 8 | `dte` is a WIP date-time processing language with focus on broad interpretation. 9 | 10 | If you don't think it's intuitive, it's most likely unfinished. 11 | 12 | It is strongly inspired by [pdd](https://github.com/jarun/pdd). 13 | 14 | ![demo](https://i.imgur.com/S7HfZZN.gif) 15 | 16 | ## How to use & What to know 17 | 18 | `pip install dte` 19 | 20 | ### Conventions 21 | ![relevant xkcd](https://sslimgs.xkcd.com/comics/iso_8601.png) 22 | - When there is margin for ambiguity, expressions are always interpreted with [highest units appearing before, complying with ISO-8601](https://preview.redd.it/2vjzrsib7ci61.png?width=2800&format=png&auto=webp&s=944b5176432419338cb2b13aeac10e61da1221f9), e.g.: `2021-06-13`, `2023 August 27` or `2019 Jul 20` 23 | - Unix timestamps are both interpreted and output in seconds by default, but this is configurable 24 | - When specifying time, just remember that `M` is for month and `m` is for minute 25 | 26 | ## Configuration File 27 | 28 | `dte` tries to read a `config.json` file under config directory (`~/.config/dte/` on Linux). In it you can set the following options: 29 | 30 | ``` 31 | { 32 | "timestamp_unit": "" 33 | "clock": "<24|12>", 34 | "datetime_output_format": ">", 35 | "comparison_tolerance_seconds": , 36 | "basedate_output_format": "%Y-%b", 37 | "decimal_places": 38 | } 39 | ``` 40 | 41 | ### Dependencies 42 | - [dateutil](https://github.com/dateutil/dateutil) handles month and year's complex operations 43 | - [ply](https://github.com/dabeaz/ply) is a pure-Python implementation of the popular compiler construction tools lex and yacc 44 | - [appdirs](https://github.com/ActiveState/appdirs) for reading config file in a cross-platform manner 45 | 46 | ## To do 47 | - [ ] format(timepoint, fmt) (in keyword) units given current time field 48 | - [ ] add custom/OS locale support (?) 49 | - [ ] add tab-completion for: 50 | - [ ] months 51 | - [ ] units given current datetime field or RHS of `in` keyword 52 | - [ ] "days until winter" 53 | - [ ] run tests across a variety of locales 54 | - [ ] add `show` function 55 | - [ ] show clock for time 56 | - [ ] show cal for date and basedate 57 | 58 | # Examples 59 | The following examples are generated based on tests run, so many results will be relative to the day it was tested. Every expression on the left side is valid syntax. 60 | 61 | [//]: <> (BEGIN EXAMPLES) 62 | 63 | |INPUT| OUTPUT | 64 | |-----|--------| 65 | |`jan 1 + 99999M`|`8334-04-01`| 66 | |`1970 january 1st`|`1970-01-01`| 67 | |`day 13 friday in 2021`|`2021-08-13`| 68 | |`friday day < 8 in Jan 2015`|`2015-01-02`| 69 | |`friday day 13 in August 2021.weekday`|`Friday`| 70 | |`friday day 13 in August 2021`|`2021-08-13`| 71 | |`2020-01-29 + (1 year + 1 month)`|`2021-02-28`| 72 | |`days until Jan 2030`|`2401.04`| 73 | |`last sun in 2021`|`2021-12-26`| 74 | |`april+1M`|`2023-05-01`| 75 | |`t+1d 08h30`|`2023-06-06 08:30:00`| 76 | |`1am t == t 1am`|`True`| 77 | |`1am t`|`2023-06-05 01:00:00`| 78 | |`(2020-10-10+1d) 3pm`|`2020-10-11 15:00:00`| 79 | |`t 1:00 == t 1am`|`True`| 80 | |`t 1:00`|`2023-06-05 01:00:00`| 81 | |`t 1am`|`2023-06-05 01:00:00`| 82 | |`august`|`2023-Aug`| 83 | |`4th wed in august`|`2023-08-23`| 84 | |`5th sunday in 2021`|`2021-01-31`| 85 | |`4th sunday in 2021`|`2021-01-24`| 86 | |`3rd sunday in 2021`|`2021-01-17`| 87 | |`2nd sunday in 2021`|`2021-01-10`| 88 | |`-1d + 2020-10-10`|`2020-10-09`| 89 | |`2014 Jan + 1M`|`2014-02-01`| 90 | |`Jan 2014 + 1M`|`2014-02-01`| 91 | |`seconds in 24h`|`86400.00`| 92 | |`today==mon`|`True`| 93 | |`days until mon`|`-0.96`| 94 | |`days until next mon`|`6.04`| 95 | |`next mon + 1d`|`2023-06-13`| 96 | |`monday+1d`|`2023-05-30`| 97 | |`weekday t+100d`|`Wednesday`| 98 | |`(weekday t+100d)==100d.weekday`|`True`| 99 | |`(weekday t+100d)`|`Wednesday`| 100 | |`weekday tm`|`Tuesday`| 101 | |`yesterday==thu`|`False`| 102 | |`yesterday==thursday`|`False`| 103 | |`last fri in Dec 2014`|`2014-12-26`| 104 | |`last fri in 2014 Dec`|`2014-12-26`| 105 | |`last fri in 2014 December`|`2014-12-26`| 106 | |`days until 2030-12-25`|`2759.04`| 107 | |`6pm+1h`|`19:00:00`| 108 | |`2014 01`|`2014-Jan`| 109 | |`1st friday in april`|`2023-04-07`| 110 | |`first friday in april`|`2023-04-07`| 111 | |`1st friday in next month`|`2023-07-07`| 112 | |`first friday in next month`|`2023-07-07`| 113 | |`next month`|`2023-07-01`| 114 | |`seconds until 11 pm`|`85931.98`| 115 | |`seconds until tomorrow`|`3131.80`| 116 | |`1996 August 28 9 AM`|`1996-08-28 09:00:00`| 117 | |`2s2s`|`0:00:04`| 118 | |`1 hour in seconds`|`3600.00`| 119 | |`1h in seconds`|`3600.00`| 120 | |`5m+5m`|`0:10:00`| 121 | |`1957-12-26 22:22:22 in unix`|`-379118258`| 122 | |`yd-5h`|`2023-06-03 19:00:00`| 123 | |`1st sun in April 2021`|`2021-04-04`| 124 | |`first sun in April 2021`|`2021-04-04`| 125 | |`1st friday in April 2014`|`2014-04-04`| 126 | |`first friday in April 2014`|`2014-04-04`| 127 | |`Jan 2014`|`2014-Jan`| 128 | |`weekday 0`|`Wednesday`| 129 | |`wait until (n+.001s)`|``| 130 | |`wait .001s`|``| 131 | |`t - next Sunday`|`-6 days, 0:00:00`| 132 | |`2012-12-13-3y.weekday`|`Sunday`| 133 | |`1st sunday in 2021`|`2021-01-03`| 134 | |`first sunday in 2021`|`2021-01-03`| 135 | |`last sunday in 2021`|`2021-12-26`| 136 | |`last Sunday != next sunday`|`True`| 137 | |`last Sunday == next sunday`|`False`| 138 | |`next Sunday != last sunday`|`True`| 139 | |`next Sunday == last sunday`|`False`| 140 | |`seconds since 3000 Apr 10`|`-30826227127.30`| 141 | |`seconds until 3000 Apr 10`|`30826227127.12`| 142 | |`2000-10-10 16:00`|`2000-10-10 16:00:00`| 143 | |`2000-10-10 00:16`|`2000-10-10 00:16:00`| 144 | |`next Sunday`|`2023-06-11`| 145 | |`n`|`2023-06-05 23:07:53.607236`| 146 | |`YD.day`|`4`| 147 | |`T.weekday`|`Monday`| 148 | |`T.day`|`5`| 149 | |`T-10d`|`2023-05-26`| 150 | |`T-1.5d`|`2023-06-03 12:00:00`| 151 | |`3M`|`3 months`| 152 | |`3h+3M`|`3 months, 3:00:00`| 153 | |`2h2m`|`2:02:00`| 154 | |`7y6M5w4d3h2m1.1s`|`7 years, 6 months, 39 days, 3:02:01`| 155 | |`1M1d`|`1 month, 1 day, 0:00:00`| 156 | |`-1y2M`|`-1 year, -2 months`| 157 | |`0y2M`|`2 months`| 158 | |`1y2M`|`1 year, 2 months`| 159 | |`6y5M4d3h2m1s`|`6 years, 5 months, 4 days, 3:02:01`| 160 | |`22h22m`|`22:22:00`| 161 | |`22h+2m`|`22:02:00`| 162 | |`12h:00 pm != 12h:00 am`|`True`| 163 | |`2 < 1`|`False`| 164 | |`2020 Jan 27 + 1y == 2021 Jan 27`|`True`| 165 | |`1w`|`7 days, 0:00:00`| 166 | |`1970 Jan 1 - 3h in unix`|`0`| 167 | |`1d1m in hours`|`24.02`| 168 | |`1d+0h22m`|`1 day, 0:22:00`| 169 | |`1d`|`1 day, 0:00:00`| 170 | |`1d in seconds`|`86400.00`| 171 | |`1d in minutes`|`1440.00`| 172 | |`1d in hours`|`24.00`| 173 | |`1958-05-14 - 1958-05-16`|`-2 days, 0:00:00`| 174 | |`1957-12-26 22:22:22 - t`|`-23902 days, 22:22:22`| 175 | |`1957-12-26 - t`|`-23902 days, 0:00:00`| 176 | |`2014 Jan 13==2014 January 13`|`True`| 177 | |`12h:00 AM != 12h:00 PM`|`True`| 178 | |`1610494238.weekday`|`Tuesday`| 179 | |`1610494238+4h.weekday`|`Wednesday`| 180 | |`1610494238`|`2021-01-12 20:30:38`| 181 | |`1-1-1-1-1-1`|`0:00:00`| 182 | |`22m:22 + 4h`|`4:22:22`| 183 | |`6pm`|`18:00:00`| 184 | |`6 pm + 1h`|`19:00:00`| 185 | |`6 pm`|`18:00:00`| 186 | |`2020-Jan-27`|`2020-01-27`| 187 | |`22:22:22`|`22:22:22`| 188 | |`22:22:22s`|`22:22:22`| 189 | |`22h:22:22s`|`22:22:22`| 190 | |`22:22m:22s`|`22:22:22`| 191 | |`22h:22m:22s`|`22:22:22`| 192 | |`22h:22m:22`|`22:22:22`| 193 | |`22h:22`|`22:22:00`| 194 | |`1996.04.28`|`1996-04-28`| 195 | |`2014 January 13`|`2014-01-13`| 196 | |`2014 Jan 13`|`2014-01-13`| 197 | |`11:20s PM`|`00:11:20`| 198 | |`11h:20m pm`|`23:20:00`| 199 | |`11h:20 am`|`11:20:00`| 200 | |`11m:20 PM`|`00:11:20`| 201 | |`11h:20 AM`|`11:20:00`| 202 | |`1-1-1 23:23S`|`0001-01-01 00:23:23`| 203 | |`1-1-1 23m:23S`|`0001-01-01 00:23:23`| 204 | |`1-1-1 23m:23s`|`0001-01-01 00:23:23`| 205 | |`1-1-1 23m:23`|`0001-01-01 00:23:23`| 206 | |`1-1-1 23h:23m`|`0001-01-01 23:23:00`| 207 | |`1-1-1 23h:23`|`0001-01-01 23:23:00`| 208 | |`1-1-1 23:23m`|`0001-01-01 23:23:00`| 209 | |`1-1-1 23:23:23`|`0001-01-01 23:23:23`| 210 | |`seconds until 2021 feb 14 12:00:00`|`-72702485.53`| 211 | |`2021 feb 14 12:00:00`|`2021-02-14 12:00:00`| 212 | |`10h30 + 14h`|`1 day, 0:30:00`| 213 | |`n - 1234`|`19514 days, 1:47:32.086022`| 214 | |`1m in hours`|`0.02`| 215 | |`1 in unix`|`1`| 216 | |`08h30`|`8:30:00`| 217 | |`-1d.weekday`|`Sunday`| 218 | |`(t + 180d)-180d == t`|`True`| 219 | |`(n + 181d)-180d != n`|`True`| 220 | |`(n + 180d)-180d == n`|`True`| 221 | |`(T-1d).weekday`|`Sunday`| 222 | -------------------------------------------------------------------------------- /bnf.txt: -------------------------------------------------------------------------------- 1 | ::= wait until 2 | | wait 3 | 4 | ::= ; 5 | 6 | ::= weekday 7 | 8 | ::= = 9 | 10 | ::= 11 | 12 | ::= 13 | 14 | ::= 15 | 16 | ::= + 17 | | - 18 | | > 19 | | < 20 | | >= 21 | | <= 22 | | == 23 | | != 24 | 25 | ::= in 26 | | in 27 | | in 28 | | in 29 | | 30 | 31 | ::= 32 | | month 33 | 34 | ::= until 35 | | since 36 | | since 37 | | until 38 | 39 | ::= 40 | | 41 | | 42 | 43 | ::= 44 | 45 | ::= in 46 | 47 | ::= 48 | | 49 | | 50 | | 51 | | 52 | | 53 | 54 | ::= . weekday 55 | 56 | ::= . 57 | 58 | ::= - 59 | 60 | ::= day 61 | | 62 | | day < 63 | | day > 64 | | day <= 65 | | day >= 66 | | day = 67 | | 68 | | 69 | 70 | ::= to 71 | | to 72 | | to 73 | | to 74 | | to 75 | | to 76 | | to 77 | | to 78 | | to 79 | 80 | ::= in 81 | | in 82 | | in 83 | -------------------------------------------------------------------------------- /distribute: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | rm -r dist dte.egg-info 3 | new_version=`grep version setup.py | \ 4 | sed -r "s/.*version='([^']*)'.*/\1/g" | \ 5 | awk -F. -v OFS=. 'NF==1{print ++$NF}; NF>1{if(length($NF+1)>length($NF))$(NF-1)++; $NF=sprintf("%0*d", length($NF), ($NF+1)%(10^length($NF))); print}'` 6 | sed -i "s/\(.*version='\).*',/\1$new_version',/g" setup.py 7 | python3 ./setup.py sdist && \ 8 | python3 -m twine upload dist/* 9 | -------------------------------------------------------------------------------- /dte/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mvrozanti/dte/5215ed75552e3b141e0d544ac3e02ea69723f3d6/dte/__init__.py -------------------------------------------------------------------------------- /dte/dte: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from enum import Enum 3 | from collections import OrderedDict 4 | from datetime import datetime, date, timedelta, time 5 | from dateutil.relativedelta import relativedelta, MO, TU, WE, TH, FR, SA, SU 6 | from dateutil.rrule import rrule, YEARLY 7 | from json.decoder import JSONDecodeError 8 | from ply.lex import TOKEN 9 | from time import sleep, mktime 10 | from types import SimpleNamespace as Namespace 11 | import appdirs 12 | import calendar 13 | import code # noqa: F401 14 | import dateutil 15 | import json 16 | import os 17 | import os.path as op 18 | import re 19 | import sys 20 | import traceback 21 | 22 | import signal 23 | signal.signal(signal.SIGINT, lambda s, _: sys.exit(0)) 24 | 25 | def exception_handler(func): 26 | def inner_function(*args, **kwargs): 27 | try: 28 | func(*args, **kwargs) 29 | except TypeError: 30 | print('='*10) 31 | traceback.print_exc() 32 | print('='*10) 33 | return inner_function 34 | 35 | def read_write_get_config(): 36 | config_dir = appdirs.user_config_dir() 37 | dte_config_dir = config_dir + '/dte' 38 | dte_config_file = dte_config_dir + '/config.json' 39 | default_config = { 40 | "timestamp_unit": "seconds", 41 | "clock": "24", 42 | "datetime_output_format": "ISO8601", 43 | "comparison_tolerance_seconds": 0.001, 44 | "basedate_output_format": "%Y-%b", 45 | "decimal_places": 2, 46 | } 47 | if not op.exists(dte_config_dir): 48 | os.mkdir(dte_config_dir) 49 | 50 | if op.exists(dte_config_file): 51 | try: 52 | config_dict = json.load(open(dte_config_file)) 53 | updated = False 54 | for k, v in default_config.items(): 55 | if k not in config_dict: 56 | config_dict.update({k: v}) 57 | updated = True 58 | if updated: 59 | json.dump(config_dict, open(dte_config_file, 'w'), indent=4) 60 | except JSONDecodeError: 61 | print('Configuration file is malformed.\n' + 62 | 'Falling back to default configuration', file=sys.stderr) 63 | config_dict = default_config 64 | else: 65 | json.dump(default_config, open(dte_config_file, 'w'), indent=4) 66 | config_dict = default_config 67 | return config_dict 68 | 69 | 70 | config = Namespace(**read_write_get_config()) 71 | 72 | HELP = ''' 73 | SYNTAX 74 | 75 | 76 | OBJECTS 77 | 78 | DELTA 79 | 80 | a timedelta object can be interpreted as 81 | chain of (amount, unit) consisting of a 82 | number followed by a time unit in ISO format, 83 | with case input relaxed except for 84 | differentiating months and minutes: 85 | 86 | 1D+1d # case insensitive 87 | 88 | 3M+3m # except for months and minutes 89 | 90 | -2m+2s # accepts negative numbers 91 | 92 | 10Y3s # join them together instead of adding 93 | 94 | 1 hour 3 minutes # fully spelled out durations 95 | | are also interepreted 96 | 97 | 10h30 # omit the last unit and dte will 98 | | interpret it as the immediately 99 | | smaller unit 100 | 101 | DATETIME 102 | 103 | a datetime object represents a point in 104 | time. Can be interpreted in various forms 105 | such as follows: 106 | 107 | 1611269086 # unix timestamp in seconds 108 | 2020/12/31 22:22 109 | 2020 Jan 12 110 | 2020 December 20 111 | 2020/12/31 22:22:22 112 | 2020/12/31 113 | today 114 | 115 | it can also represent time only: 116 | 117 | 22:22:22 118 | 22:22 119 | 22h:22 120 | 22H:22 121 | 22m:22 122 | 22:22s 123 | 22:22m 124 | 6 pm 125 | 6h:20 am 126 | 6h20 am 127 | 128 | BASEDATE 129 | 130 | a basedate object is a point in time that 131 | represents the beginning of a month: 132 | 133 | 2014 Jan 134 | Jan 2014 135 | 136 | INTEGERS 137 | 138 | Integers by themselves are interpreted as unix timestamps. 139 | 140 | 1 : is interpreted as the point in time one second 141 | after Epoch. 142 | 143 | VARIABLES 144 | 145 | there are four built-in variables, all 146 | case insensitive: 147 | 148 | T or today 149 | YD or yesterday 150 | TM or tomorrow 151 | N or now 152 | 153 | but you can also assign objects to a 154 | named variable, like so: 155 | foo=1d 156 | bar=YD 157 | 158 | OPERATORS 159 | + : adds deltas to points in time 160 | - : takes de difference between two points 161 | in time and stores a delta 162 | 163 | < <= > == != : compares two points in time or two durations 164 | and returns a boolean 165 | 166 | KEYWORDS 167 | in : the `in` keyword has two purposes, 168 | interpreting a DELTA object and a unit 169 | and converting the former into the latter 170 | such as: 171 | 172 | 1d3h in seconds 173 | 174 | it can also convert a point in time to a 175 | unix timestamp, which can be configured in 176 | seconds or milliseconds using the config 177 | file: 178 | 179 | 1970 Jan 1 in unix 180 | 181 | until: takes a point in time and a unit and shows 182 | the the amount of the unit until the time 183 | point: 184 | 185 | seconds until 3000 Apr 25 186 | 187 | FUNCTIONS / ATTRIBUTES 188 | 189 | wait DELTA : sleeps for the duration 190 | 191 | wait UNTIL TIME_POINT: sleeps until the time point 192 | 193 | next WEEKDAY : returns the date for the next weekday 194 | 195 | last WEEKDAY : returns the date for the last weekday 196 | 197 | weekday TIME_POINT : returns weekday for time point 198 | TIME_POINT.weekday | 199 | 200 | STDIN & ARGUMENTS 201 | 202 | Both the standard input and the arguments can be used to run expressions 203 | 204 | ''' 205 | 206 | n = None 207 | 208 | 209 | def replace(replacee, replacement, string): 210 | return re.sub(replacee, replacement, string) 211 | 212 | 213 | relativedelta_days = [MO, TU, WE, TH, FR, SA, SU] 214 | 215 | days = list(calendar.day_name) 216 | months = list(calendar.month_name)[1:] 217 | days_abbrev = list(calendar.day_abbr) 218 | months_abbrev = list(calendar.month_abbr)[1:] 219 | 220 | days_en = [ 221 | 'Monday', 222 | 'Tuesday', 223 | 'Wednesday', 224 | 'Thursday', 225 | 'Friday', 226 | 'Saturday', 227 | 'Sunday' 228 | ] 229 | days_en_abbrev = [d[:3] for d in days_en] 230 | days_en_abbrev_max = [d[:2] for d in days_en] 231 | months_en = [ 232 | 'January', 233 | 'February', 234 | 'March', 235 | 'April', 236 | 'May', 237 | 'June', 238 | 'July', 239 | 'August', 240 | 'September', 241 | 'October', 242 | 'November', 243 | 'December' 244 | ] 245 | months_en_abbrev = [m[:3] for m in months_en] 246 | 247 | tokens = ( 248 | 'PLUS', 'MINUS', 'EQUALS', 249 | 'LPAREN', 'RPAREN', 250 | 'UNIT', 251 | 'INDEXABLE_OP', 252 | 'IN', 253 | 'WAIT', 254 | 'UNTIL', 255 | 'SINCE', 256 | 'GT', 'GE', 'LT', 'LE', 'EQ', 'NE', 257 | 'OR', 'AND', 258 | 'TO', 259 | 'NAME', 260 | 'ORDINAL', 261 | 'INTEGER', 262 | 'YEAR', 263 | 'MONTH', 264 | 'MONTH_LITERAL', 265 | 'WEEKDAY_LITERAL', 266 | 'BASEDATE', 267 | 'DELTA', 268 | 'WEEKDAY', 269 | 'PERIOD', 270 | 'SEMICOLON', 271 | 'COLON', 272 | 'DATETIME', 273 | ) 274 | 275 | # Tokens 276 | 277 | t_COLON = r':' 278 | t_SEMICOLON = r';' 279 | t_PERIOD = r'\.' 280 | t_PLUS = r'\+' 281 | t_MINUS = r'-' 282 | t_EQUALS = r'=' 283 | t_LPAREN = r'\(' 284 | t_RPAREN = r'\)' 285 | t_GT = r'>' 286 | t_GE = r'>=' 287 | t_LT = r'<' 288 | t_LE = r'<=' 289 | t_EQ = r'==' 290 | t_NE = r'!=' 291 | t_IN = r'(?i:in)' 292 | t_UNTIL = r'(?i:until)' 293 | t_SINCE = r'(?i:since)' 294 | t_WAIT = r'(?i:wait)' 295 | t_MONTH_LITERAL = r'(?i:month)' 296 | t_WEEKDAY_LITERAL = r'(?i:weekday)' 297 | t_OR = '(?i:or)' 298 | t_AND = '(?i:and)' 299 | t_TO = '(?i:to)' 300 | 301 | reserved = [ 302 | r'in', 303 | r'next|last', 304 | r'seconds|minutes|hours|days|weeks', 305 | r'wait', 306 | r'until', 307 | r'and|or', 308 | r'to(?!morrow|day)', 309 | r'since', 310 | r'month', 311 | r'weekday', 312 | r'unix', 313 | ] 314 | 315 | 316 | def is_reserved(k): 317 | for r in reserved: 318 | if re.match('(?i)'+r, k): 319 | return True 320 | 321 | 322 | REGEX_ALL_MONTHS = r'|'.join(months) + r'|' + \ 323 | r'|'.join(months_abbrev) + r'|' + \ 324 | r'|'.join(months_en_abbrev) 325 | REGEX_DOY = r'(\d+(?!:)(?:\W)\d+(?!:)(?:\W)\d+|\d+[\W\s]+(?:' + \ 326 | REGEX_ALL_MONTHS + \ 327 | r')(?:\s|[^\w\+])+\d+)(?:st|nd|rd|th)?' 328 | REGEX_0_23 = r'(2[0-3]|1?[0-9]|00)' 329 | REGEX_0_59 = r'([0-5]?[0-9])' 330 | 331 | DATETIME_REGEX = \ 332 | fr'(?i:(?:{REGEX_DOY}\s?|' + \ 333 | r'(?:(1[0-2]|[0-9])\s*([aApP][mM])|' + \ 334 | fr'{REGEX_0_59}([hHm])?' + \ 335 | f'(?::{REGEX_0_59}([msS])?' + \ 336 | f'(?::(?:{REGEX_0_59}([sS])?))?' + \ 337 | r'))(?:\s*([aApP][mM]))?' + \ 338 | '){1,2})' 339 | 340 | @TOKEN(DATETIME_REGEX) 341 | def t_DATETIME(t): 342 | date_str,\ 343 | zeroth_val,\ 344 | zeroth_ampm,\ 345 | first_val,\ 346 | first_unit,\ 347 | second_val,\ 348 | second_unit,\ 349 | third_val,\ 350 | third_unit,\ 351 | ampm = re.search(DATETIME_REGEX, t.value).groups() 352 | 353 | if zeroth_val and zeroth_ampm: 354 | le_time = datetime.strptime( 355 | f'{zeroth_val} {zeroth_ampm}', '%I %p').time() 356 | if not date_str: 357 | t.value = le_time 358 | return t 359 | else: 360 | le_time = None 361 | 362 | if t.value.count(':') == 1 and \ 363 | not first_unit and not second_unit \ 364 | and not third_unit and not third_val: 365 | first_unit = 'h' 366 | 367 | if ampm is not None and \ 368 | (first_unit == 'h' or 369 | first_unit == 'H' or 370 | second_unit == 'm' or 371 | third_val) and \ 372 | int(first_val) > 12: 373 | print('Conflicting 24-hour time in 12-hour clock', file=sys.stderr) 374 | 375 | if date_str: 376 | if any(month.lower() in date_str.lower() 377 | for month in months_abbrev + months): 378 | y, b, d = replace(r'[\W\s]+', ' ', date_str).split(' ') 379 | try: 380 | date = datetime.strptime(f'{y.zfill(4)}-{b}-{d}', '%Y-%b-%d') 381 | except ValueError: 382 | try: 383 | date = datetime.strptime( 384 | f'{y.zfill(4)}-{b}-{d}', '%Y-%B-%d') 385 | except ValueError: 386 | print(f'Invalid syntax: {date_str}', file=sys.stderr) 387 | else: 388 | y, M, d = replace(r'\D', '-', date_str).split('-') 389 | date = datetime.strptime( 390 | f'{y.zfill(4)}-{M}-{d}', '%Y-%m-%d').date() 391 | if [zeroth_val, first_val, second_val, third_val] == [None]*4: 392 | t.value = date 393 | return t 394 | else: 395 | date = datetime.today() 396 | semicolon_count = t.value.count(':') 397 | is_HMS = semicolon_count == 2 398 | is_HM = 'h' == first_unit or \ 399 | 'H' == first_unit or \ 400 | 'm' == second_unit or \ 401 | (third_unit is not None and 402 | ('s' == third_unit or 'S' == third_unit)) 403 | if ampm is not None and \ 404 | (first_unit == 'h' or first_unit == 'H' or 405 | second_unit == 'm'): 406 | if first_val != '12': 407 | if ampm.lower() == 'pm': 408 | first_val = f'{(int(first_val)+12)}' 409 | else: 410 | if ampm.lower() == 'am': 411 | first_val = 0 412 | 413 | H_or_M, M_or_S, S = (first_val, second_val, third_val) 414 | if is_HMS: 415 | le_time = datetime.strptime( 416 | f'{H_or_M}:{M_or_S}:{S}', '%H:%M:%S').time() 417 | elif is_HM: 418 | le_time = datetime.strptime(f'{H_or_M}:{M_or_S}', '%H:%M').time() 419 | elif le_time is not None: 420 | pass 421 | else: 422 | le_time = datetime.strptime(f'{H_or_M}:{M_or_S}', '%M:%S').time() 423 | t.value = le_time if not date_str else datetime.combine(date, le_time) 424 | return t 425 | 426 | 427 | unit_map = { 428 | 's': 'seconds', 429 | 'S': 'seconds', 430 | 'm': 'minutes', 431 | 'h': 'hours', 432 | 'H': 'hours', 433 | 'd': 'days', 434 | 'D': 'days', 435 | 'w': 'weeks', 436 | 'W': 'weeks', 437 | 'M': 'months', 438 | 'y': 'years', 439 | 'Y': 'years', 440 | } 441 | 442 | SHORT_UNITS_STR = ''.join(unit_map.keys()) 443 | LONG_UNITS_STR = '(?:'+'?|'.join(set(unit_map.values()))+'?)' 444 | FLOATING_POINT = r'((?:\d*[.])?\d+)' 445 | EXCLUDE_TAIL = '(?!ec|t|ep|ay)' 446 | UNFINISHED_LAST_UNIT_DELTA = \ 447 | fr'(?:[ \t]*{FLOATING_POINT}\s*({LONG_UNITS_STR}|[{SHORT_UNITS_STR}]{EXCLUDE_TAIL})' 448 | OMITTABLE_LAST_UNIT_DELTA = f'{UNFINISHED_LAST_UNIT_DELTA}?)' 449 | DELTA_TOKEN = f'{UNFINISHED_LAST_UNIT_DELTA}){OMITTABLE_LAST_UNIT_DELTA}*' 450 | 451 | 452 | @TOKEN(DELTA_TOKEN) 453 | def t_DELTA(t): 454 | units_vals = OrderedDict() 455 | matches = re.findall(OMITTABLE_LAST_UNIT_DELTA, t.value) 456 | for v, u in matches: 457 | unit_key = u if u in 'mM' else u.lower() 458 | if unit_key in units_vals: 459 | units_vals[unit_key] += float(v) if v else 1 460 | else: 461 | units_vals.update({unit_key: float(v) if v else 1}) 462 | t.value = parse_units(units_vals) 463 | if '' in units_vals: 464 | u, _ = list(units_vals.items())[list(units_vals.keys()).index('')-1] 465 | next_unit_index = SHORT_UNITS_STR.index(u)-1 466 | if next_unit_index == -1: 467 | print('Invalid delta', file=sys.stderr) 468 | next_unit = SHORT_UNITS_STR[next_unit_index] 469 | if next_unit.lower() == u.lower(): 470 | next_unit = SHORT_UNITS_STR[SHORT_UNITS_STR.index(u)-2] 471 | t.value += parse_units({next_unit: units_vals['']}) 472 | return t 473 | 474 | 475 | def get_month_index_by_name(month_name): 476 | for month_collection in (months, months_abbrev, months_en_abbrev): 477 | for month in month_collection: 478 | if month.lower() == month_name.lower(): 479 | return month_collection.index(month) + 1 480 | 481 | 482 | t_NAME = '(?!' + '|'.join(reserved) + ')([a-zA-Z_][a-zA-Z0-9_]*)' 483 | 484 | # @TOKEN(_NAME) 485 | # def t_NAME(t): 486 | # return t 487 | 488 | 489 | REGEX_1_12 = '(1[0-2]|[1-9])' 490 | REGEX_1_12_OPTIONALLY_PADDED = '(1[0-2]|0?[0-9])' 491 | BASEDATE_REGEX = r'(?i:(?:' + \ 492 | r'('+REGEX_ALL_MONTHS+r')(?!:|\+|;)\W+(\d+)|' + \ 493 | r'(\d+)(?!:|\+|;)\W('+REGEX_ALL_MONTHS+')|' + \ 494 | r'(\d+)(?!:|\+|;)\W'+REGEX_1_12_OPTIONALLY_PADDED + \ 495 | '))' 496 | 497 | @TOKEN(BASEDATE_REGEX) 498 | def t_BASEDATE(t): 499 | m1, y1, y2, m2, y3, m3 = re.search(BASEDATE_REGEX, t.value).groups() 500 | if m1: 501 | month = m1 502 | year = y1 503 | if m2: 504 | month = m2 505 | year = y2 506 | if m3: 507 | month = m3 508 | year = y3 509 | year = int(year) 510 | month_index = get_month_index_by_name(month) 511 | if not month_index: 512 | month_index = int(month) 513 | t.value = Basedate(year, month_index) 514 | return t 515 | 516 | 517 | def t_UNIT(t): 518 | r'(?i:seconds|minutes|hours|days|weeks|months|years|unix)' 519 | return t 520 | 521 | 522 | WEEKDAY_TOKEN = r'(?i:Monday|Tuesday|Wednesday|Thursday|Friday|' + \ 523 | 'Saturday|Sunday|Mon(?!th)|Tue|Wed|Thu|Fri|Sat|Sun)' 524 | 525 | 526 | @TOKEN(WEEKDAY_TOKEN) 527 | def t_WEEKDAY(t): 528 | t.value = Weekday(t.value) 529 | return t 530 | 531 | 532 | @TOKEN(r'(?i:' + '|'.join(months) + '|'.join(months_abbrev) + ')') 533 | def t_MONTH(t): 534 | t.value = Month(t.value) 535 | return t 536 | 537 | 538 | class Indexable(Enum): 539 | FIRST = 1 540 | SECOND = 2 541 | THIRD = 3 542 | FOURTH = 4 543 | FIFTH = 5 544 | NEXT = 0 545 | LAST = float("inf") 546 | PREVIOUS = -1 547 | 548 | 549 | ordinalIndexables = [ 550 | Indexable.FIRST, 551 | Indexable.SECOND, 552 | Indexable.THIRD, 553 | Indexable.FOURTH, 554 | Indexable.FIFTH 555 | ] 556 | 557 | 558 | def t_INDEXABLE_OP(t): 559 | r'(?i:(next|last|first|prev(ious)?|1st|2nd|3rd|4th|5th|first|second|third|fourth|fifth))' 560 | t.value = t.value.lower() 561 | if t.value.startswith('previous'): 562 | t.value = Indexable.PREVIOUS 563 | if t.value == 'next': 564 | t.value = Indexable.NEXT 565 | if t.value == 'last': 566 | t.value = Indexable.LAST 567 | if t.value in ['first', '1st']: 568 | t.value = Indexable.FIRST 569 | if t.value in ['second', '2nd']: 570 | t.value = Indexable.SECOND 571 | if t.value in ['third', '3rd']: 572 | t.value = Indexable.THIRD 573 | if t.value in ['fourth', '4th']: 574 | t.value = Indexable.FOURTH 575 | if t.value in ['fifth', '5th']: 576 | t.value = Indexable.FIFTH 577 | return t 578 | 579 | 580 | def get_closest_month(month): 581 | counter_next = 0 582 | counter_prev = 0 583 | next_date = names['n'] 584 | for _ in range(7): 585 | next_date += relativedelta(months=1) 586 | counter_next += 1 587 | cur_month_name = months[next_date.month-1].lower() 588 | short_cur_month_name = cur_month_name[:3] 589 | if month.name in [cur_month_name, short_cur_month_name]: 590 | break 591 | prev_date = names['n'] 592 | for _ in range(7): 593 | prev_date += relativedelta(months=-1) 594 | counter_prev += 1 595 | cur_month_name = months[prev_date.month-1].lower() 596 | short_cur_month_name = cur_month_name[:3] 597 | if month.name in [cur_month_name, short_cur_month_name]: 598 | break 599 | if counter_next < counter_prev: 600 | return next_date.date().replace(day=1) 601 | return prev_date.date().replace(day=1) 602 | 603 | 604 | def get_closest_week_day(week_day): 605 | counter_next = 0 606 | counter_prev = 0 607 | next_date = names['n'] 608 | if days[next_date.weekday()] == week_day.lower() or \ 609 | days_abbrev[next_date.weekday()].lower() == week_day.lower(): 610 | return next_date.date() 611 | else: 612 | for _ in range(7): 613 | next_date += timedelta(days=1) 614 | counter_next += 1 615 | if days[next_date.weekday()].lower() == week_day.lower(): 616 | break 617 | if days_abbrev[next_date.weekday()].lower() == week_day.lower(): 618 | break 619 | prev_date = names['n'] 620 | for _ in range(7): 621 | prev_date += timedelta(days=-1) 622 | counter_prev += 1 623 | if days[prev_date.weekday()].lower() == week_day.lower(): 624 | break 625 | if days_abbrev[prev_date.weekday()].lower() == week_day.lower(): 626 | break 627 | if counter_next < counter_prev: 628 | return next_date.date() 629 | return prev_date.date() 630 | 631 | 632 | def parse_units(units_vals): 633 | parsed = timedelta() 634 | for unit, val in units_vals.items(): 635 | if unit == '': 636 | continue 637 | short = unit in unit_map 638 | singular = unit + 's' in set(unit_map.values()) 639 | if singular: 640 | unit += 's' 641 | if short: 642 | if unit.lower() == 'y': 643 | parsed += relativedelta(years=units_vals[unit]) 644 | elif unit == 'M': 645 | parsed += relativedelta(months=units_vals[unit]) 646 | else: 647 | parsed += timedelta(**{unit_map[unit]: val}) 648 | else: 649 | try: 650 | parsed += timedelta(**{unit: val}) 651 | except TypeError: 652 | parsed += relativedelta(**{unit: val}) 653 | return parsed 654 | 655 | 656 | def t_INTEGER(t): 657 | r'\d+' 658 | t.value = int(t.value) 659 | return t 660 | 661 | 662 | t_ignore = ' \t' 663 | t_ignore_COMMENT = r'\s*\#.*' 664 | 665 | 666 | def t_newline(t): 667 | r'\n+' 668 | t.lexer.lineno += t.value.count('\n') 669 | 670 | 671 | def t_error(t): 672 | print(f'Illegal character {t.value[0]!r}', file=sys.stderr) 673 | t.lexer.skip(1) 674 | 675 | 676 | import ply.lex as lex # noqa: E402 677 | lex.lex(debug=False) 678 | 679 | def wait(t): 680 | now = names['n'] 681 | if isinstance(t, datetime): 682 | delta = t - now 683 | elif isinstance(t, date): 684 | delta = datetime.combine(t, datetime.min.time()) - now 685 | elif isinstance(t, time): 686 | delta = datetime.combine(now.date(), t) - now 687 | elif isinstance(t, timedelta): 688 | delta = t 689 | else: 690 | print('Wait accepts a time point or time delta only', file=sys.stderr) 691 | if delta > timedelta(0): 692 | sleep(delta.total_seconds()) 693 | 694 | 695 | def weekday(t): 696 | if type(t) == date or type(t) == datetime: 697 | return days[t.weekday()] 698 | elif type(t) == timedelta: 699 | return days[(names['n']+t).weekday()] 700 | elif type(t) == list: 701 | return [weekday(e) for e in t] 702 | else: 703 | print('Can\'t get day of week of object of type' , file=sys.stderr+ 704 | str(type(t))) 705 | 706 | 707 | def is_000(obj): 708 | return obj.hour == obj.minute == obj.second == 0 if type(obj) == datetime \ 709 | else type(obj) == date 710 | 711 | 712 | class Weekday: 713 | def __init__(self, name): 714 | self.name = name 715 | 716 | def __str__(self): 717 | return self.name 718 | 719 | 720 | class Month: 721 | def __init__(self, name): 722 | self.name = name 723 | 724 | def __str__(self): 725 | return self.name 726 | 727 | 728 | class Basedate: 729 | def __init__(self, year, month): 730 | self.year = year 731 | self.month = month 732 | 733 | def to_datetime(self): 734 | return datetime(self.year, self.month, 1) 735 | 736 | def __str__(self): 737 | return datetime(self.year, self.month, 1).strftime( 738 | config.basedate_output_format) 739 | 740 | 741 | names = { 742 | 'day': lambda t: t.day, 743 | 'month': lambda t: t.month, 744 | 'year': lambda t: t.year, 745 | 'hour': lambda t: t.hour, 746 | 'minute': lambda t: t.minute, 747 | 'second': lambda t: t.second, 748 | 'weekday': lambda t: weekday(t), 749 | 'dayofweek': lambda t: weekday(t), 750 | 'help': lambda: print(HELP), 751 | } 752 | 753 | 754 | precedence = ( 755 | ('right', 756 | 'UMINUS', 757 | ), # noqa: E124 758 | ('left', 759 | 'UNIT', 760 | 'PLUS', 761 | 'MINUS', 762 | ), # noqa: E124 763 | ) 764 | 765 | 766 | def p_statement_wait(p): 767 | '''statement : WAIT UNTIL expression 768 | | WAIT DELTA 769 | ''' 770 | if len(p) > 3: 771 | wait(p[3]) 772 | else: 773 | wait(p[2]) 774 | 775 | 776 | def p_statements(p): 777 | 'statement : statement SEMICOLON statement' 778 | 779 | 780 | def p_expression_weekday_literal_expression(p): 781 | '''expression : WEEKDAY_LITERAL expression 782 | ''' 783 | p[0] = weekday(p[2]) 784 | 785 | 786 | def p_statement_invalid_assignment(p): 787 | '''statement : WEEKDAY EQUALS expression 788 | ''' 789 | print(f'Can\'t assign expression to {p[1]} keyword', file=sys.stderr) 790 | 791 | @exception_handler 792 | def statement_assign(p): 793 | global n 794 | n = None 795 | if is_reserved(p[1]): 796 | print('Can\'t use reserved keyword', file=sys.stderr) 797 | names[p[1]] = p[3] 798 | 799 | def p_statement_assign(p): 800 | 'statement : NAME EQUALS expression' 801 | statement_assign(p) 802 | 803 | 804 | def normalize(t): 805 | if type(t) == datetime and \ 806 | is_000(t): 807 | t = t.date() 808 | if type(t) == relativedelta: 809 | tr = [] 810 | if t.years != 0: 811 | tr += [f'{t.years} year{"s" if abs(t.years) != 1 else ""}'] 812 | if t.months != 0: 813 | tr += [f'{t.months} month{"s" if abs(t.months) != 1 else ""}'] 814 | if t.days == t.hours == t.minutes == t.seconds == 0: 815 | ta = '' 816 | else: 817 | ta = str(timedelta(days=t.days, hours=t.hours, minutes=t.minutes, seconds=t.seconds)) 818 | t = '' 819 | if tr: 820 | t += ', '.join(tr) 821 | if ta: 822 | t += ', ' + ta 823 | return t 824 | 825 | @exception_handler 826 | def expression_expression_point(p): 827 | try: 828 | p[0] = datetime.combine(p[1], p[2]) 829 | except TypeError: 830 | p[0] = datetime.combine(p[2], p[1]) 831 | 832 | def p_expression_expression_point(p): 833 | 'expression : expression point' 834 | expression_expression_point(p) 835 | 836 | @exception_handler 837 | def point_name(p): 838 | if p[1] not in names: 839 | print(f'{p[1]} is not loaded') 840 | return 841 | if callable(names[p[1]]): 842 | p[0] = normalize(names[p[1]]()) 843 | else: 844 | p[0] = names[p[1]] 845 | 846 | def p_point_name(p): 847 | 'point : NAME' 848 | point_name(p) 849 | 850 | @exception_handler 851 | def statement_expr(p): 852 | if type(p[1]) is Weekday: 853 | p[1] = get_closest_week_day(str(p[1])) 854 | if type(p[1]) is Month: 855 | closest_month = get_closest_month(p[1]) 856 | p[1] = Basedate(closest_month.year, closest_month.month) 857 | if type(p[1]) == dateutil._common.weekday: 858 | p[1] = (names['n'] - relativedelta(weekday=p[1])).date() 859 | if p[1] is not None: 860 | if config.datetime_output_format != 'ISO8601': 861 | print(normalize(p[1]).strftime(config.datetime_output_format)) 862 | elif config.clock != '24' and type(p[1]) == time: 863 | print(normalize(p[1]).strftime('%I %p')) 864 | elif isinstance(p[1], float): 865 | print('{:.{d}f}'.format(p[1], d=config.decimal_places)) 866 | elif type(p[1]) == list: 867 | print('\r\n'.join([str(normalize(e)) for e in p[1]])) 868 | else: 869 | print(normalize(p[1])) 870 | names['_'] = p[1] 871 | 872 | 873 | def p_statement_expr(p): 874 | 'statement : expression' 875 | statement_expr(p) 876 | 877 | def p_filter(p): 878 | '''filter : NAME INTEGER 879 | | NAME LT INTEGER 880 | | NAME GT INTEGER 881 | | NAME LE INTEGER 882 | | NAME GE INTEGER 883 | | NAME EQUALS INTEGER 884 | | WEEKDAY 885 | | filter filter 886 | ''' 887 | # | NAME NE INTEGER 888 | p[0] = p[1:] 889 | 890 | 891 | def p_range(p): 892 | '''range : INTEGER TO INTEGER 893 | | BASEDATE TO BASEDATE 894 | | DATETIME TO BASEDATE 895 | | BASEDATE TO DATETIME 896 | | DATETIME TO DATETIME 897 | | BASEDATE TO INTEGER 898 | | INTEGER TO BASEDATE 899 | ''' 900 | p[0] = p[1:] 901 | 902 | def parse_filter(filtr): 903 | filter_args = {} 904 | for condition in filtr: 905 | if type(condition) == Weekday: 906 | condition = [condition] 907 | if type(condition[0]) == Weekday: 908 | if 'byweekday' not in filter_args: 909 | filter_args['byweekday'] = [] 910 | try: 911 | weekday_ix = [wd.lower() for wd in days].index(condition[0].name) 912 | except ValueError: 913 | weekday_ix = [wd.lower() for wd in days_abbrev].index(condition[0].name) 914 | filter_args['byweekday'] += [relativedelta_days[weekday_ix]] 915 | elif condition[0] == 'day': 916 | if 'bymonthday' not in filter_args: 917 | filter_args['bymonthday'] = [] 918 | if condition[1] == '>': 919 | filter_args['bymonthday'] += list(range(condition[2]+1,32)) 920 | elif condition[1] == '<': 921 | filter_args['bymonthday'] += list(range(1,condition[2])) 922 | elif condition[1] == '<=': 923 | filter_args['bymonthday'] += list(range(1,condition[2]+1)) 924 | elif condition[1] == '>=': 925 | filter_args['bymonthday'] += list(range(condition[2],32)) 926 | elif condition[1] == '==': 927 | filter_args['bymonthday'] += [condition[2]] 928 | elif condition[1] == '!=': 929 | filter_args['bymonthday'] += [d for d in range(0,32) if d != condition[2]] 930 | else: 931 | filter_args['bymonthday'] += [condition[1]] 932 | return filter_args 933 | 934 | 935 | def resolve_range(r): 936 | assert r[1] == 'to' 937 | s,e = None,None 938 | if type(r[0]) == Basedate: 939 | s = r[0].to_datetime() 940 | if type(r[2]) == Basedate: 941 | e = r[2].to_datetime() 942 | if type(r[0]) == int: 943 | s = datetime(r[0], 1, 1) 944 | if type(r[2]) == int: 945 | e = datetime(r[2], 1, 1) 946 | return s,e 947 | 948 | @exception_handler 949 | def statement_predicate_in(p): 950 | filter_args = parse_filter(p[1]) 951 | if type(p[3]) == Basedate: 952 | filter_args['dtstart'] = p[3].to_datetime() 953 | filter_args['until'] = p[3].to_datetime() + \ 954 | relativedelta(months=1) - timedelta(days=1) 955 | if type(p[3]) == list and p[3][1] == 'to': # range 956 | start, end = resolve_range(p[3]) 957 | filter_args['dtstart'] = start 958 | filter_args['until'] = end 959 | if type(p[3]) == int: # year 960 | filter_args['dtstart'] = datetime(p[3],1,1) 961 | filter_args['until'] = datetime(p[3]+1,1,1)-timedelta(days=1) 962 | p[0] = list(rrule(YEARLY, **filter_args)) 963 | 964 | def p_statement_predicate_in(p): 965 | '''expression : filter IN INTEGER 966 | | filter IN BASEDATE 967 | | filter IN range 968 | ''' 969 | statement_predicate_in(p) 970 | 971 | 972 | def time2timedelta(n): 973 | return timedelta(hours=n.hour, minutes=n.minute, seconds=n.second) 974 | 975 | @exception_handler 976 | def expression_binop(p): 977 | if type(p[1]) == str: 978 | p[1] = Weekday(p[1]) 979 | 980 | if type(p[3]) == str: 981 | p[3] = Weekday(p[3]) 982 | 983 | if type(p[1]) == Month: 984 | p[1] = get_closest_month(p[1]) 985 | 986 | if type(p[3]) == Month: 987 | p[3] = get_closest_month(p[3]) 988 | 989 | if type(p[3]) == Basedate: 990 | p[3] = p[3].to_datetime() 991 | 992 | if type(p[1]) == Basedate: 993 | p[1] = p[1].to_datetime() 994 | 995 | if type(p[1]) == time and type(p[3]) == timedelta: 996 | p[1] = time2timedelta(p[1]) 997 | 998 | if type(p[3]) == time and type(p[1]) == timedelta: 999 | p[3] = time2timedelta(p[3]) 1000 | 1001 | if type(p[3]) == date and type(p[1]) == timedelta: 1002 | p[3] = datetime.combine(p[3], datetime.min.time()) 1003 | 1004 | if type(p[1]) == date and type(p[3]) == timedelta: 1005 | p[1] = datetime.combine(p[1], datetime.min.time()) 1006 | 1007 | if type(p[1]) == date and type(p[3]) == datetime: 1008 | p[1] = datetime.combine(p[1], datetime.min.time()) 1009 | 1010 | if type(p[3]) == date and type(p[1]) == datetime: 1011 | p[3] = datetime.combine(p[3], datetime.min.time()) 1012 | 1013 | if type(p[1]) == type(p[3]) == date and p[2] == '+': # noqa: E721 1014 | print(f'Can\'t add two dates: {p[1]} + {p[3]}', file=sys.stderr) 1015 | 1016 | if p[1] is None or p[3] is None: 1017 | print(f'In {p[2]} expression, both operands are None', file=sys.stderr) 1018 | 1019 | if type(p[1]) == Weekday: 1020 | p[1] = get_closest_week_day(str(p[1])) 1021 | if type(p[3]) == Weekday: 1022 | p[3] = get_closest_week_day(str(p[3])) 1023 | if p[2] == '+': 1024 | p[0] = p[1] + p[3] 1025 | elif p[2] == '-': 1026 | p[0] = p[1] - p[3] 1027 | 1028 | def p_expression_binop(p): 1029 | '''expression : expression PLUS expression 1030 | | expression MINUS expression 1031 | ''' 1032 | expression_binop(p) 1033 | 1034 | @exception_handler 1035 | def expression_comparison(p): 1036 | if type(p[1]) == date and type(p[3]) == datetime: 1037 | p[1] = datetime.combine(p[1], datetime.min.time()) 1038 | 1039 | if type(p[3]) == date and type(p[1]) == datetime: 1040 | p[3] = datetime.combine(p[3], datetime.min.time()) 1041 | 1042 | if type(p[1]) == Weekday and type(p[3]) == date: 1043 | p[0] = weekday(p[3]).lower().startswith(p[1].name.lower()) 1044 | if p[2] == '!=': 1045 | p[0] = not p[0] 1046 | elif p[2] == '==': 1047 | pass 1048 | else: 1049 | print('To be implemented', file=sys.stderr) 1050 | return 1051 | 1052 | if type(p[3]) == Weekday and type(p[1]) == date: 1053 | p[0] = weekday(p[1]).lower().startswith(p[3].name.lower()) 1054 | if p[2] == '!=': 1055 | p[0] = not p[0] 1056 | elif p[2] == '==': 1057 | pass 1058 | else: 1059 | print('To be implemented', file=sys.stderr) 1060 | return 1061 | 1062 | try: 1063 | if p[2] == '<': 1064 | p[0] = p[1] < p[3] 1065 | if p[2] == '>': 1066 | p[0] = p[1] > p[3] 1067 | if p[2] == '>=': 1068 | p[0] = p[1] >= p[3] 1069 | if p[2] == '<=': 1070 | p[0] = p[1] <= p[3] 1071 | if p[2] == '==': 1072 | try: 1073 | p[0] = p[1] == p[3] or \ 1074 | abs((p[3] - p[1]).total_seconds()) < \ 1075 | config.comparison_tolerance_seconds 1076 | except TypeError: 1077 | p[0] = False 1078 | if p[2] == '!=': 1079 | p[0] = p[1] != p[3] 1080 | if type(p[1]) == datetime and \ 1081 | is_000(p[1]): 1082 | p[1] = p[1].date() 1083 | if type(p[3]) == datetime and \ 1084 | is_000(p[3]): 1085 | p[3] = p[3].date() 1086 | except TypeError as e: 1087 | print(str(e)) 1088 | 1089 | 1090 | def p_expression_comparison(p): 1091 | '''expression : expression GT expression 1092 | | expression LT expression 1093 | | expression GE expression 1094 | | expression LE expression 1095 | | expression EQ expression 1096 | | expression NE expression 1097 | ''' 1098 | expression_comparison(p) 1099 | 1100 | 1101 | def get_extremity_weekday_of_year(direction, weekday, year): 1102 | if type(weekday) is Weekday: 1103 | weekday = weekday.name 1104 | if direction == Indexable.LAST: 1105 | target = date(year+1, 1, 1) - timedelta(days=1) 1106 | elif direction in ordinalIndexables: 1107 | target = date(year, 1, 1) 1108 | try: 1109 | weekday_ix = [wd.lower() for wd in days].index(weekday) 1110 | except ValueError: 1111 | weekday_ix = [wd.lower() for wd in days_abbrev].index(weekday) 1112 | while target.weekday() != weekday_ix: 1113 | if direction in ordinalIndexables: 1114 | target += timedelta(days=1) 1115 | elif direction == Indexable.LAST: 1116 | target -= timedelta(days=1) 1117 | if direction == Indexable.SECOND: 1118 | target += timedelta(days=7) 1119 | if direction == Indexable.THIRD: 1120 | target += timedelta(days=14) 1121 | if direction == Indexable.FOURTH: 1122 | target += timedelta(days=21) 1123 | if direction == Indexable.FIFTH: 1124 | target += timedelta(days=28) 1125 | return target 1126 | 1127 | 1128 | def get_extremity_weekday_of_basedate(direction, weekday, basedate): 1129 | if type(weekday) is Weekday: 1130 | weekday = weekday.name 1131 | if direction == Indexable.LAST: 1132 | target = basedate + relativedelta(months=1, days=-1) 1133 | elif direction in ordinalIndexables: 1134 | target = basedate 1135 | try: 1136 | weekday_ix = [wd.lower() for wd in days].index(weekday) 1137 | except ValueError: 1138 | weekday_ix = [wd.lower() for wd in days_abbrev].index(weekday) 1139 | while target.weekday() != weekday_ix: 1140 | if direction in ordinalIndexables: 1141 | target += timedelta(days=1) 1142 | elif direction == Indexable.LAST: 1143 | target -= timedelta(days=1) 1144 | if direction == Indexable.SECOND: 1145 | target += timedelta(days=7) 1146 | if direction == Indexable.THIRD: 1147 | target += timedelta(days=14) 1148 | if direction == Indexable.FOURTH: 1149 | target += timedelta(days=21) 1150 | if direction == Indexable.FIFTH: 1151 | target += timedelta(days=28) 1152 | if target.month == basedate.month: 1153 | return target 1154 | 1155 | 1156 | def common_weekday_to_string(common_weekday): 1157 | for ix, d in enumerate(relativedelta_days): 1158 | if d == common_weekday: 1159 | return days[ix] 1160 | 1161 | 1162 | def string_to_common_weekday(weekday): 1163 | for ix, d in enumerate(days): 1164 | if d.lower() == weekday.lower(): 1165 | return relativedelta_days[ix] 1166 | 1167 | 1168 | def cyclic(t, direction): 1169 | cyclic_direction = names['n'] 1170 | found = False 1171 | for i in range(7): 1172 | cyclic_direction += timedelta(days=direction) 1173 | if days[cyclic_direction.weekday()].lower().startswith(str(t).lower()): 1174 | found = True 1175 | break 1176 | if not found: 1177 | print('Cyclic operation fatal error', file=sys.stderr) 1178 | return cyclic_direction.date() 1179 | 1180 | 1181 | def get_relative_basedate(direction): 1182 | cyclic_direction = names['n'] 1183 | reference_month = cyclic_direction.month 1184 | while reference_month == cyclic_direction.month: 1185 | cyclic_direction += timedelta(days=direction) 1186 | return datetime(cyclic_direction.year, cyclic_direction.month, 1) 1187 | 1188 | @exception_handler 1189 | def point_relativeindex(p): 1190 | direction, operand = p[1] 1191 | if len(p) > 2: 1192 | if type(p[3]) == int: 1193 | p[0] = get_extremity_weekday_of_year(direction, operand, p[3]) 1194 | if type(p[3]) == Basedate: 1195 | p[3] = p[3].to_datetime() 1196 | if type(p[3]) == datetime: 1197 | p[0] = get_extremity_weekday_of_basedate(direction, operand, p[3]) 1198 | if type(p[3]) == Month: 1199 | closest_month = get_closest_month(p[3]) 1200 | p[0] = get_extremity_weekday_of_basedate( 1201 | direction, operand, closest_month) 1202 | if type(p[3]) == tuple: 1203 | direction2, operand2 = p[3] 1204 | leap2 = 1 if direction2 == Indexable.NEXT else -1 1205 | p[3] = get_relative_basedate(leap2) 1206 | p_point_relativeindex(p) 1207 | elif direction in [Indexable.NEXT, Indexable.LAST]: 1208 | leap = 1 if direction == Indexable.NEXT else -1 1209 | if operand == 'month': 1210 | p[0] = get_relative_basedate(leap) 1211 | else: 1212 | p[0] = cyclic(operand, leap) 1213 | 1214 | def p_point_relativeindex(p): 1215 | '''point : relativeindex IN MONTH 1216 | | relativeindex IN INTEGER 1217 | | relativeindex IN BASEDATE 1218 | | relativeindex IN relativeindex 1219 | | relativeindex 1220 | ''' 1221 | point_relativeindex(p) 1222 | 1223 | 1224 | def p_relativeindex_indexable_op(p): 1225 | '''relativeindex : INDEXABLE_OP WEEKDAY 1226 | | INDEXABLE_OP MONTH_LITERAL 1227 | ''' 1228 | p[0] = (p[1], p[2]) 1229 | 1230 | 1231 | def delta_to_unit(delta, unit): 1232 | total_seconds = delta.total_seconds() 1233 | if unit == 'seconds': 1234 | return total_seconds 1235 | if unit == 'minutes': 1236 | return total_seconds / 60 1237 | if unit == 'hours': 1238 | return total_seconds / 60 / 60 1239 | if unit == 'days': 1240 | return total_seconds / 60 / 60 / 24 1241 | if unit == 'weeks': 1242 | return total_seconds / 60 / 60 / 24 / 7 1243 | if unit in ['months', 'years']: 1244 | print('Invalid conversion', file=sys.stderr) 1245 | 1246 | @exception_handler 1247 | def expression_unit_until_point(p): 1248 | if p[1].lower() in unit_map.values(): 1249 | if type(p[3]) == str: 1250 | if p[3] in names: 1251 | p[3] = datetime.combine(names[p[3]], datetime.min.time()) 1252 | else: 1253 | print('{p[3]} is unrecognized', file=sys.stderr) 1254 | if type(p[3]) == Weekday: 1255 | p[3] = get_closest_week_day(p[3].name) 1256 | if type(p[3]) == Basedate: 1257 | p[3] = p[3].to_datetime() 1258 | if type(p[3]) == datetime: 1259 | delta = p[3] - names['n'] 1260 | timeflag = False 1261 | if type(p[3]) == time: 1262 | delta = datetime.combine(datetime.today(), p[3]) - names['n'] 1263 | timeflag = True 1264 | if type(p[3]) == date: 1265 | delta = datetime.combine(p[3], datetime.min.time()) - \ 1266 | names['n'] 1267 | 1268 | p[0] = delta_to_unit(delta, p[1].lower()) 1269 | if p[2] == 'since': 1270 | p[0] = -p[0] 1271 | if timeflag: 1272 | if p[0] < 0: 1273 | p[3] = datetime.combine(datetime.today(), p[3]) + \ 1274 | timedelta(days=1 if p[2] == 'until' else -1) 1275 | p_expression_unit_until_point(p) 1276 | 1277 | else: 1278 | print('Invalid syntax: {p[1]} {p[2]} {p[3]}', file=sys.stderr) 1279 | 1280 | def p_expression_unit_until_point(p): 1281 | '''expression : UNIT UNTIL expression 1282 | | UNIT SINCE expression 1283 | | UNIT SINCE NAME 1284 | | UNIT UNTIL NAME 1285 | ''' 1286 | expression_unit_until_point(p) 1287 | 1288 | def p_expression_generic(p): 1289 | '''expression : DELTA 1290 | | timestamp 1291 | | point 1292 | ''' 1293 | p[0] = p[1] 1294 | 1295 | @exception_handler 1296 | def timestamp_integer(p): 1297 | if config.timestamp_unit == 'seconds': 1298 | ts = int(p[1]) 1299 | else: 1300 | ts = int(p[1])/1000 1301 | p[0] = datetime.fromtimestamp(ts) 1302 | 1303 | def p_timestamp_integer(p): 1304 | 'timestamp : INTEGER' 1305 | timestamp_integer(p) 1306 | 1307 | 1308 | def p_expression_point_in_unit(p): 1309 | 'expression : UNIT IN DELTA' 1310 | aux = p[3] 1311 | p[3] = p[1] 1312 | p[1] = aux 1313 | p_statement_expression_in_unit(p) 1314 | 1315 | 1316 | @exception_handler 1317 | def statement_expression_in_unit(p): 1318 | to_unix = p[3] == 'unix' 1319 | if type(p[1]) == timedelta and to_unix: 1320 | print('Can\'t convert timedelta to unix timestamp', file=sys.stderr) 1321 | if to_unix: 1322 | p[0] = int(mktime(p[1].timetuple())) 1323 | if p[3].lower() in unit_map.values(): 1324 | p[0] = delta_to_unit(p[1], p[3].lower()) 1325 | 1326 | def p_statement_expression_in_unit(p): 1327 | 'expression : expression IN UNIT' 1328 | statement_expression_in_unit(p) 1329 | 1330 | 1331 | def p_point(p): 1332 | '''point : timestamp 1333 | | BASEDATE 1334 | | DATETIME 1335 | | MONTH 1336 | | WEEKDAY 1337 | | YEAR 1338 | ''' 1339 | p[0] = p[1] 1340 | 1341 | 1342 | def p_expression_get_weekday(p): 1343 | 'expression : expression PERIOD WEEKDAY_LITERAL' 1344 | p[0] = weekday(p[1]) 1345 | 1346 | 1347 | def p_expression_get_attribute(p): 1348 | 'expression : expression PERIOD NAME' 1349 | p[0] = names[p[3]](p[1]) 1350 | 1351 | 1352 | def p_expression_group(p): 1353 | 'expression : LPAREN expression RPAREN' 1354 | p[0] = p[2] 1355 | 1356 | 1357 | def p_expression_uminus(p): 1358 | 'expression : MINUS DELTA %prec UMINUS' 1359 | p[0] = -p[2] 1360 | 1361 | 1362 | import ply.yacc as yacc # noqa: E402 1363 | yacc.yacc(errorlog=yacc.NullLogger()) 1364 | 1365 | 1366 | def current_time_routine(): 1367 | for n in ['n', 'now', 'N', 'NOW']: 1368 | names[n] = datetime.now() 1369 | for t in ['t', 'today', 'T', 'TODAY']: 1370 | names[t] = datetime.today().date() 1371 | for tm in ['tm', 'tomorrow', 'TOMORROW', 'TM']: 1372 | names[tm] = datetime.today().date() + timedelta(days=1) 1373 | for yd in ['yd', 'yesterday', 'YESTERDAY', 'YD']: 1374 | names[yd] = datetime.today().date() - timedelta(days=1) 1375 | 1376 | 1377 | def interactive(): 1378 | import cmd 1379 | 1380 | class CmdParse(cmd.Cmd): 1381 | prompt = '' 1382 | commands = [] 1383 | 1384 | def default(self, line): 1385 | if line == 'EOF': 1386 | exit(0) 1387 | if line.startswith('#'): 1388 | return 1389 | current_time_routine() 1390 | yacc.parse(line) 1391 | self.commands.append(line) 1392 | 1393 | def do_help(self, line): 1394 | print(HELP) 1395 | 1396 | def do_exit(self, line): 1397 | return True 1398 | CmdParse().cmdloop() 1399 | 1400 | 1401 | if __name__ == '__main__': 1402 | if len(sys.argv) > 1: 1403 | if sys.argv[1] in ['-h', '--help']: 1404 | print(HELP) 1405 | else: 1406 | current_time_routine() 1407 | yacc.parse(' '.join(sys.argv[1:])) 1408 | else: 1409 | interactive() 1410 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | ply 2 | python-dateutil 3 | appdirs 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import setuptools 3 | import os 4 | 5 | with open('README.md', 'r') as fh: 6 | long_description = fh.read() 7 | 8 | setuptools.setup( 9 | name='dte', 10 | version='0.3.2', 11 | author="Marcelo V. Rozanti", 12 | author_email="mvrozanti@hotmail.com", 13 | description="Date Time Expressions", 14 | long_description=long_description, 15 | long_description_content_type="text/markdown", 16 | url="https://github.com/mvrozanti/dte", 17 | packages=setuptools.find_packages('dte'), 18 | install_requires=[ 19 | 'python-dateutil', 'ply', 'appdirs' 20 | ], 21 | scripts=[ 22 | 'dte/dte', 23 | ], 24 | classifiers=[ 25 | "Development Status :: 4 - Beta", 26 | "Topic :: Artistic Software", 27 | "Intended Audience :: Developers", 28 | "Programming Language :: Python :: 3.6", 29 | "Programming Language :: Python :: 3.7", 30 | "Programming Language :: Python :: 3.8", 31 | "Programming Language :: Python :: 3.9", 32 | "Programming Language :: Python :: 3.10", 33 | "Programming Language :: Python :: 3.11", 34 | "Programming Language :: Python :: 3 :: Only", 35 | ], 36 | ) 37 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mvrozanti/dte/5215ed75552e3b141e0d544ac3e02ea69723f3d6/test/__init__.py -------------------------------------------------------------------------------- /test/test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S python -m pytest 2 | from collections import OrderedDict 3 | from subprocess import Popen, PIPE, call 4 | import code # noqa 5 | import os 6 | from os import path as op 7 | import re 8 | import unittest 9 | 10 | days = ['Monday', 'Tuesday', 'Wednesday', 11 | 'Thursday', 'Friday', 'Saturday', 'Sunday'] 12 | 13 | ISO_FORMAT = r'^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(\.\d+)?$' 14 | 15 | YMD_FORMAT = r'^\d{4}-\d{2}-\d{2}$' 16 | 17 | YMD_LIST_FORMAT = r'(\d{4}-\d{2}-\d{2}|\n)*' 18 | 19 | DAY_LIST_FORMAT = r'(' + '|'.join(days) + '|\n)*' 20 | 21 | HMS_FORMAT = r'^\d+:\d+:\d+$' 22 | 23 | DELTA_FORMAT = r'(-?\d+ years?, (-?\d+ months?(, )?)? ?)?(-?\d+ days?, \d{1,2}:\d{2}:\d{2})?' 24 | 25 | BASEDATE_FORMAT = r'\d+-\w+' 26 | 27 | test_expectancy = OrderedDict({ 28 | # 'days since easter' : lambda 29 | # 'days until easter' : lambda 30 | # 'easter 2014' : lambda 31 | # 'fridays in 2015' # rrule 32 | # 'fris in 2015' # rrule 33 | # 'friday day = 8 in Jan 2015' : lambda r: r == '2015-01-02', 34 | 35 | # '2014 Jan 3:00' : lambda r: False, 36 | # 'Jan 2014 3:00' : lambda r: False, 37 | 'jan 1 + 99999M' : lambda r: r.endswith('01'), 38 | '1970 january 1st' : lambda r: re.match(YMD_LIST_FORMAT, r), 39 | 'day 13 friday in 2021' : lambda r: re.match(YMD_LIST_FORMAT, r), 40 | 'friday day < 13 in 2014 sep' : lambda r: re.match(YMD_LIST_FORMAT, r), 41 | 'friday in Jan 2015' : lambda r: re.match(YMD_LIST_FORMAT, r), 42 | 'friday day < 8 in Jan 2015' : lambda r: r == '2015-01-02', 43 | 'friday day > 8 in Jan 2015 to Jan 2019': lambda r: re.match(YMD_LIST_FORMAT, r), 44 | 'friday day < 8 in Jan 2015 to Jan 2019': lambda r: re.match(YMD_LIST_FORMAT, r), 45 | 'friday day 13 in Jan 2015 to Jan 2019' : lambda r: re.match(YMD_LIST_FORMAT, r), 46 | 'friday day 13 in August 2021.weekday' : lambda r: r == 'Friday', 47 | 'friday day 13 in August 2021' : lambda r: r == '2021-08-13', 48 | 'friday day 13 in 2015.weekday' : lambda r: re.match(DAY_LIST_FORMAT, r), 49 | 'friday day 13 in 2015' : lambda r: re.match(YMD_LIST_FORMAT, r), 50 | 'fri in 2015' : lambda r: re.match(YMD_LIST_FORMAT, r), 51 | 'friday in 2015' : lambda r: re.match(YMD_LIST_FORMAT, r), 52 | '2020-01-29 + (1 year + 1 month)' : lambda r: r == '2021-02-28', 53 | 'days until Jan 2030' : lambda r: float(r) < 3002, 54 | 'last sun in 2021' : lambda r: r == '2021-12-26', 55 | 'april+1M' : lambda r: re.match(YMD_FORMAT, r), 56 | 't+1d 08h30' : lambda r: re.match(ISO_FORMAT, r), 57 | '1am t == t 1am' : lambda r: eval(r), 58 | '1am t' : lambda r: re.match(ISO_FORMAT, r), 59 | '(2020-10-10+1d) 3pm' : lambda r: '2020-10-11 15:00:00' == r, 60 | 't 1:00 == t 1am' : lambda r: eval(r), 61 | 't 1:00' : lambda r: re.match(ISO_FORMAT, r), 62 | 't 1am' : lambda r: re.match(ISO_FORMAT, r), 63 | 'august' : lambda r: re.match(BASEDATE_FORMAT, r), 64 | '4th wed in august' : lambda r: re.match(YMD_FORMAT, r), 65 | '5th sunday in 2021' : lambda r: r == '2021-01-31', 66 | '4th sunday in 2021' : lambda r: r == '2021-01-24', 67 | '3rd sunday in 2021' : lambda r: r == '2021-01-17', 68 | '2nd sunday in 2021' : lambda r: r == '2021-01-10', 69 | '-1d + 2020-10-10' : lambda r: r == '2020-10-09', 70 | '2014 Jan + 1M' : lambda r: re.match(YMD_FORMAT, r), 71 | 'Jan 2014 + 1M' : lambda r: re.match(YMD_FORMAT, r), 72 | 'seconds in 24h' : lambda r: float(r) == 86400, 73 | 'today==mon' : lambda r: r in ['True', 'False'], 74 | 'days until mon' : lambda r: -4.5 < float(r) < 4.5, 75 | 'days until next mon' : lambda r: 0 <= float(r) <= 7, 76 | 'next mon + 1d' : lambda r: re.match(YMD_FORMAT, r), 77 | 'monday+1d' : lambda r: re.match(YMD_FORMAT, r), 78 | 'weekday t+100d' : lambda r: r in days, 79 | '(weekday t+100d)==100d.weekday' : lambda r: eval(r), 80 | '(weekday t+100d)' : lambda r: r in days, 81 | 'weekday tm' : lambda r: r in days, 82 | 'yesterday==thu' : lambda r: r in ['True', 'False'], 83 | 'yesterday==thursday' : lambda r: r in ['True', 'False'], 84 | 'last fri in Dec 2014' : lambda r: re.match(YMD_FORMAT, r), 85 | 'last fri in 2014 Dec' : lambda r: re.match(YMD_FORMAT, r), 86 | 'last fri in 2014 December' : lambda r: re.match(YMD_FORMAT, r), 87 | 'days until 2030-12-25' : lambda r: float(r) < 3364.55, 88 | '6pm+1h' : lambda r: re.match(HMS_FORMAT, r), 89 | '2014 01' : lambda r: re.match(BASEDATE_FORMAT, r), 90 | '1st friday in april' : lambda r: re.match(YMD_FORMAT, r) and r.split('-')[1] == '04', 91 | 'first friday in april' : lambda r: re.match(YMD_FORMAT, r) and r.split('-')[1] == '04', 92 | '1st friday in next month' : lambda r: re.match(YMD_FORMAT, r), 93 | 'first friday in next month' : lambda r: re.match(YMD_FORMAT, r), 94 | 'next month' : lambda r: re.match(BASEDATE_FORMAT, r), 95 | 'seconds until 11 pm' : lambda r: float(r) < 86400, 96 | 'seconds until tomorrow' : lambda r: 0 < float(r) < 86400, 97 | '1996 August 28 9 AM' : lambda r: r == '1996-08-28 09:00:00', 98 | '2s2s' : lambda r: r == '0:00:04', 99 | '1 hour in seconds' : lambda r: r == '3600.00', 100 | '1h in seconds' : lambda r: r == '3600.00', 101 | '5m+5m' : lambda r: r == '0:10:00', 102 | '1957-12-26 22:22:22 in unix' : lambda r: -379118258 - 86400 < int(r) < -379118258 + 86400, 103 | 'yd-5h' : lambda r: re.match(ISO_FORMAT, r), 104 | '1st sun in April 2021' : lambda r: r == '2021-04-04', 105 | 'first sun in April 2021' : lambda r: r == '2021-04-04', 106 | '1st friday in April 2014' : lambda r: r == '2014-04-04', 107 | 'first friday in April 2014' : lambda r: r == '2014-04-04', 108 | 'Jan 2014' : lambda r: re.match(BASEDATE_FORMAT, r), 109 | 'weekday 0' : lambda r: r in ['Wednesday', 'Thursday'], 110 | 'wait until (n+.001s)' : lambda r: len(r) == 0, 111 | 'wait .001s' : lambda r: len(r) == 0, 112 | 't - next Sunday' : lambda r: re.match(DELTA_FORMAT, r), 113 | '2012-12-13-3y.weekday' : lambda r: r == 'Sunday', 114 | '1st sunday in 2021' : lambda r: r == '2021-01-03', 115 | 'first sunday in 2021' : lambda r: r == '2021-01-03', 116 | 'last sunday in 2021' : lambda r: r == '2021-12-26', 117 | 'last Sunday != next sunday' : lambda r: r == 'True', 118 | 'last Sunday == next sunday' : lambda r: r == 'False', 119 | 'next Sunday != last sunday' : lambda r: r == 'True', 120 | 'next Sunday == last sunday' : lambda r: r == 'False', 121 | 'seconds since 3000 Apr 10' : lambda r: -30899416627.60163 < float(r), 122 | 'seconds until 3000 Apr 10' : lambda r: 30899416627.60163 > float(r), 123 | '2000-10-10 16:00' : lambda r: r == '2000-10-10 16:00:00', 124 | '2000-10-10 00:16' : lambda r: r == '2000-10-10 00:16:00', 125 | 'next Sunday' : lambda r: re.match(YMD_FORMAT, r), 126 | 'n' : lambda r: re.match(ISO_FORMAT, r), 127 | 'YD.day' : lambda r: re.match(r'\d+', r), 128 | 'T.weekday' : lambda r: r in days, 129 | 'T.day' : lambda r: 0 < int(r) < 32, 130 | 'T-10d' : lambda r: re.match(YMD_FORMAT, r), 131 | 'T-1.5d' : lambda r: re.match(ISO_FORMAT, r), 132 | '3M' : lambda r: re.match(DELTA_FORMAT, r), 133 | '3h+3M' : lambda r: re.match(DELTA_FORMAT, r), 134 | '2h2m' : lambda r: r == '2:02:00', 135 | '7y6M5w4d3h2m1.1s' : lambda r: re.match(DELTA_FORMAT, r), 136 | '1M1d' : lambda r: r == '1 month, 1 day, 0:00:00', 137 | '-1y2M' : lambda r: re.match(DELTA_FORMAT, r), 138 | '0y2M' : lambda r: r == '2 months', 139 | '1y2M' : lambda r: r == '1 year, 2 months', 140 | '6y5M4d3h2m1s' : lambda r: re.match(DELTA_FORMAT, r), 141 | '22h22m' : lambda r: r == '22:22:00', 142 | '22h+2m' : lambda r: r == '22:02:00', 143 | '12h:00 pm != 12h:00 am' : lambda r: eval(r), 144 | '2 < 1' : lambda r: not eval(r), 145 | '2020 Jan 27 + 1y == 2021 Jan 27' : lambda r: eval(r), 146 | '1w' : lambda r: r == '7 days, 0:00:00', 147 | '1970 Jan 1 - 3h in unix' : lambda r: int(r) <= 24*60*60, 148 | '1d1m in hours' : lambda r: r == '24.02', 149 | '1d+0h22m' : lambda r: r == '1 day, 0:22:00', 150 | '1d' : lambda r: r == '1 day, 0:00:00', 151 | '1d in seconds' : lambda r: r == '86400.00', 152 | '1d in minutes' : lambda r: r == '1440.00', 153 | '1d in hours' : lambda r: r == '24.00', 154 | '1958-05-14 - 1958-05-16' : lambda r: r == '-2 days, 0:00:00', 155 | '1957-12-26 22:22:22 - t' : lambda r: re.match(DELTA_FORMAT, r), 156 | '1957-12-26 - t' : lambda r: re.match(DELTA_FORMAT, r), 157 | '2014 Jan 13==2014 January 13' : lambda r: eval(r), 158 | '12h:00 AM != 12h:00 PM' : lambda r: eval(r), 159 | '1610494238.weekday' : lambda r: r == 'Tuesday', 160 | '1610494238+4h.weekday' : lambda r: r == 'Wednesday', 161 | '1610494238' : lambda r: '2021-01-12' in r, 162 | '1-1-1-1-1-1' : lambda r: '0:00:00', 163 | '22m:22 + 4h' : lambda r: r == '4:22:22', 164 | '6pm' : lambda r: re.match(HMS_FORMAT, r), 165 | '6 pm + 1h' : lambda r: re.match(HMS_FORMAT, r), 166 | '6 pm' : lambda r: re.match(HMS_FORMAT, r), 167 | '2020-Jan-27' : lambda r: r == '2020-01-27', 168 | '22:22:22' : lambda r: re.match(HMS_FORMAT, r), 169 | '22:22:22s' : lambda r: r == '22:22:22', 170 | '22h:22:22s' : lambda r: r == '22:22:22', 171 | '22:22m:22s' : lambda r: r == '22:22:22', 172 | '22h:22m:22s' : lambda r: r == '22:22:22', 173 | '22h:22m:22' : lambda r: r == '22:22:22', 174 | '22:22:22' : lambda r: r == '22:22:22', 175 | '22h:22' : lambda r: r == '22:22:00', 176 | '1996.04.28' : lambda r: r == '1996-04-28', 177 | '2014 January 13' : lambda r: r == '2014-01-13', 178 | '2014 Jan 13' : lambda r: r == '2014-01-13', 179 | '11:20s PM' : lambda r: r == '00:11:20', 180 | '11h:20m pm' : lambda r: r == '23:20:00', 181 | '11h:20 am' : lambda r: r == '11:20:00', 182 | '11m:20 PM' : lambda r: r == '00:11:20', 183 | '11h:20 AM' : lambda r: r == '11:20:00', 184 | '1-1-1 23:23S' : lambda r: re.match(ISO_FORMAT, r), 185 | '1-1-1 23m:23S' : lambda r: re.match(ISO_FORMAT, r), 186 | '1-1-1 23m:23s' : lambda r: re.match(ISO_FORMAT, r), 187 | '1-1-1 23m:23' : lambda r: re.match(ISO_FORMAT, r), 188 | '1-1-1 23h:23m' : lambda r: re.match(ISO_FORMAT, r), 189 | '1-1-1 23h:23' : lambda r: re.match(ISO_FORMAT, r), 190 | '1-1-1 23:23m' : lambda r: re.match(ISO_FORMAT, r), 191 | '1-1-1 23:23:23' : lambda r: re.match(ISO_FORMAT, r), 192 | 'seconds until 2021 feb 14 12:00:00' : lambda r: float(r) < 580301.752936, 193 | '2021 feb 14 12:00:00' : lambda r: r == '2021-02-14 12:00:00', 194 | '10h30 + 14h' : lambda r: r == '1 day, 0:30:00', 195 | 'n - 1234' : lambda r: re.match(DELTA_FORMAT, r), 196 | '1m in hours' : lambda r: r == '0.02', 197 | '1 in unix' : lambda r: r == '1', 198 | '08h30' : lambda r: r == '8:30:00', 199 | '-1d.weekday' : lambda r: r in days, 200 | '(t + 180d)-180d == t' : lambda r: eval(r), 201 | '(n + 181d)-180d != n' : lambda r: eval(r), 202 | '(n + 180d)-180d == n' : lambda r: eval(r), 203 | '(T-1d).weekday' : lambda r: r in days, 204 | }) 205 | 206 | 207 | def run(test): 208 | dte_location = os.path.dirname(os.path.realpath(__file__)) \ 209 | + op.sep + '..' \ 210 | + (op.sep + 'dte')*2 211 | p = Popen(dte_location, stdin=PIPE, stdout=PIPE, stderr=PIPE) 212 | out, err = p.communicate(test.encode('utf-8')) 213 | out = out.decode('utf-8').replace('\n', '') 214 | return out, err 215 | 216 | 217 | def make_documentation(test_out): 218 | import pathlib 219 | le_path = pathlib.Path(__file__).parent.resolve() 220 | call(['sed', '-i', '/BEGIN EXAMPLES/q', f'{le_path}/../README.md']) 221 | with open('../README.md', 'a') as f: 222 | f.write('\n|INPUT| OUTPUT |\n') 223 | f.write('|-----|--------|\n') 224 | for test, out in test_out.items(): 225 | if '\r' not in out: 226 | f.write('|`' + test + '`|`' + out + '`|\n') 227 | 228 | 229 | class Tester(unittest.TestCase): 230 | 231 | def test_stuff(self): 232 | test_out = OrderedDict() 233 | for test, expectancy in test_expectancy.items(): 234 | try: 235 | out, err = run(test) 236 | test_out[test] = out 237 | assert expectancy(out) and not err 238 | except Exception as e: 239 | print(test) 240 | raise e 241 | make_documentation(test_out) 242 | --------------------------------------------------------------------------------