diff --git a/README.md b/README.md
index 9046c43..3fa1faf 100644
--- a/README.md
+++ b/README.md
@@ -28,6 +28,8 @@ python tww QUERY [--debug] [--full] [--show=]
- `time between 2012-03-14 and 2012-04-26`
- `time since 09:00`
- `time until end of workday`
+ - Timezone difference: `timezone difference between and ` or `tz diff between and `
+ - `timezone difference between sofia and portugal`
- Calculate time differences: ` (?\+|\-|plus|minus) ` or ` (before|from) `
- `12-12-2019 + 2 weeks`
- `05:23 - 150 minutes`
@@ -44,6 +46,9 @@ python tww QUERY [--debug] [--full] [--show=]
- `2021-05-10 day of week`
- Find timezones
- `timezone in Brazil`
+ - Print calendar `cal(endar) (month) ` or `cal year ` (for whole year)
+ - `cal year 2021`
+ - `calendar january 2018`
diff --git a/tww/lib.py b/tww/lib.py
index 230c9da..e8ce18b 100644
--- a/tww/lib.py
+++ b/tww/lib.py
@@ -290,6 +290,7 @@ def find_from_offset(query):
def resolve_timezone(query, dt=None):
if not query:
query = "utc"
+ dt = dt or datetime.now()
# if the human_tz_loc contains /, assume it's a timezone which could be
# incorrectly written with small letters - need Continent/City
normal_query = query.lower().strip()
@@ -301,6 +302,7 @@ def resolve_timezone(query, dt=None):
"normal_query": normal_query,
"tz_offset": local_offset,
"tz_name": local_iana,
+ 'is_local': True,
}
found_from_iana_tz = NORMALIZED_TZ_DICT.get(normal_query, "")
found_from_abbr_tzs = list(NORMALIZED_TZ_ABBR.get(normal_query, set()))
@@ -343,10 +345,11 @@ def resolve_timezone(query, dt=None):
except UnknownTimeZoneError:
pytz_result = type('pytz', (), {"zone": ""})
tz_name = pytz_result.zone
- tz_dst_seconds = pytz_result.dst(dt.replace(tzinfo=None) or datetime.now()).seconds
- pytz_localized = pytz_result.localize(dt.replace(tzinfo=None) or datetime.now())
+ tz_dst_seconds = pytz_result.dst(dt.replace(tzinfo=None)).seconds
+ pytz_localized = pytz_result.localize(dt.replace(tzinfo=None))
tz_abbr = pytz_localized.strftime('%Z') if tz_name else ""
tz_offset = pytz_localized.strftime('%z') if tz_name else ""
+ tz_offset_seconds = offset_to_seconds(tz_offset)
return {
"query": query,
"normal_query": normal_query,
@@ -361,8 +364,10 @@ def resolve_timezone(query, dt=None):
"tz_name": tz_name,
"tz_abbr": tz_abbr,
"tz_offset": tz_offset,
+ "tz_offset_seconds": tz_offset_seconds,
"tz_dst_seconds": tz_dst_seconds,
"tz_is_dst": tz_dst_seconds != 0,
+ "is_local": False,
}
@@ -623,6 +628,11 @@ def split_offset(offset):
return int(to_shh), int(to_mm)
+def offset_to_seconds(offset):
+ h, m = split_offset(offset)
+ return h * 3600 + m * 60
+
+
def dt_tz_translation(dt: datetime, to_tz_offset: str, from_tz_offset: str = "+00:00") -> datetime:
to_shh, to_mm = split_offset(to_tz_offset)
from_shh, from_mm = split_offset(from_tz_offset)
@@ -647,6 +657,10 @@ def get_local_tzname_iana():
def get_local_tz_offset():
+ from tww.tokenizer import custom_tz
+ global custom_tz
+ if custom_tz and not custom_tz.get('is_local'):
+ return custom_tz.get('tz_offset')
now = datetime.now(tzlocal())
if now.tzinfo._isdst(now):
return format_offset_from_timedelta(now.tzinfo._dst_offset)
diff --git a/tww/tokenizer.py b/tww/tokenizer.py
index 00e0efd..687d6c6 100644
--- a/tww/tokenizer.py
+++ b/tww/tokenizer.py
@@ -3,7 +3,8 @@ import json
import locale
import re
import logging
-from datetime import datetime
+from datetime import datetime, timedelta
+import calendar
from pygments import highlight, lexers, formatters
from scalpl import Cut
@@ -16,6 +17,7 @@ from tww.lib import resolve_timezone, dateparser_parse_dt, get_utcnow, get_s_sin
from tww.common import logger
custom_locale = resolve_locale()
+custom_tz = None
r_generic = re.compile('(.*)', flags=re.IGNORECASE)
r_time_in_epoch_s_now = re.compile('(?:time since epoch|seconds since epoch)', flags=re.IGNORECASE)
@@ -28,6 +30,7 @@ r_time_in = re.compile('(?:time)?\s*in\s*(.*)', flags=re.IGNORECASE)
r_time_since = re.compile('(?:time|year|month|week|day|hour|minute|second)?(?:s)?\s*since\s*(.*)', flags=re.IGNORECASE)
r_time_until = re.compile('(?:time|year|month|week|day|hour|minute|second)?(?:s)?\s*until\s*(.*)', flags=re.IGNORECASE)
r_time_between = re.compile('(?:time|year|month|week|day|hour|minute|second)?(?:s)?\s*between\s*(.*)\s*and\s*(.*)', flags=re.IGNORECASE)
+r_tz_between = re.compile('(?:time difference|tz diff|time diff|time zone difference|timezone difference|timezone diff|time zone diff)?(?:s)?\s*between\s*(.*)\s*and\s*(.*)', flags=re.IGNORECASE)
r_time_plus = re.compile('(.*)\s*(?:plus|\+|after|from)\s*(.*)', flags=re.IGNORECASE)
r_time_minus = re.compile('(.*)\s*(?:minus|\-)\s*(.*)', flags=re.IGNORECASE)
r_time_before = re.compile('(.*)\s*(?:before|\-)\s*(.*)', flags=re.IGNORECASE)
@@ -43,13 +46,11 @@ r_timezone_translation = re.compile('(.*)?\s(?:in|to)\s(.*)', flags=re.IGNORECAS
r_timezone_translation_in_to = re.compile('(.*)(?:in)\s(.*)\s(?:to)\s(.*)', flags=re.IGNORECASE)
r_hour_minute_timezone = re.compile('((?:[0[0-9]|1[0-9]|2[0-3]):[0-5][0-9])?\s(.*)', flags=re.IGNORECASE)
r_timezone = re.compile('(.*)\s(?:timezone|timezones|tz)', flags=re.IGNORECASE)
+r_calendar_year = re.compile('(?:cal year|calendar year)?\s*(.*)', flags=re.IGNORECASE)
+r_calendar_month = re.compile('(?:calendar|cal|month|cal month|calendar month)?\s*(.*)', flags=re.IGNORECASE)
r_timezone_2 = re.compile('(?:timezone in|timezones in|tz in|timezone|timezones|tz)\s(.*)?', flags=re.IGNORECASE)
-def handler_time(dt_s):
- return dateparser_parse_dt(dt_s)
-
-
def handler_time_now_local():
return get_local_now()
@@ -68,6 +69,15 @@ def dt_normalize(start_dt, end_dt) -> (datetime, datetime):
return start_dt, end_dt
+def handler_tz_diff(start_tz_s: str, end_tz_s: str) -> dict:
+ start_tz = resolve_timezone(start_tz_s)
+ end_tz = resolve_timezone(end_tz_s)
+ diff = timedelta(seconds=start_tz["tz_offset_seconds"] - end_tz["tz_offset_seconds"])
+ return dict(start=start_tz,
+ end=end_tz,
+ diff=td_pretty(diff))
+
+
def handler_time_diff(start_dt, end_dt) -> dict:
start_dt, end_dt = dt_normalize(start_dt, end_dt)
diff = start_dt - end_dt
@@ -182,10 +192,19 @@ def handler_time_before(td: str, dt_s: str):
return dt - td
+def handler_calendar(dt_s: str):
+ dt = dateparser_parse_dt(dt_s)
+ return {
+ "month": calendar.month(int(dt.strftime("%Y")), int(dt.strftime("%m"))),
+ "year": calendar.calendar(int(dt.strftime("%Y"))),
+ }
+
+
QUERY_TYPE_DT_TR = "datetime_translation"
QUERY_TYPE_DT = "datetime_details"
QUERY_TYPE_TZ = "timezone"
QUERY_TYPE_TD = "timedelta"
+QUERY_TYPE_CAL = "calendar"
h_default = ''
h_unix_s = 'dt->unix_s'
@@ -196,21 +215,24 @@ h_translation = 'dt->iso8601_full'
h_default_dt = 'dt->iso8601_full'
h_default_td = 'timedelta->diff->duration_human'
h_day_of_week = 'dt->locale_day_of_week'
+h_cal_year = 'cal->year'
+h_cal_month = 'cal->month'
regex_handlers = [
(r_time_in_epoch_s_now, handler_time_now_local, QUERY_TYPE_DT, h_unix_s),
(r_time_in_epoch_s_now, handler_time_now_utc, QUERY_TYPE_DT, h_unix_s),
- (r_time_in_epoch_s2, handler_time, QUERY_TYPE_DT, h_unix_s),
- (r_time_in_epoch_s3, handler_time, QUERY_TYPE_DT, h_unix_s),
+ (r_time_in_epoch_s2, handler_generic_parser, QUERY_TYPE_DT, h_unix_s),
+ (r_time_in_epoch_s3, handler_generic_parser, QUERY_TYPE_DT, h_unix_s),
(r_time_in_epoch_ms_now, handler_time_now_local, QUERY_TYPE_DT, h_unix_ms),
(r_time_in_epoch_ms_now, handler_time_now_utc, QUERY_TYPE_DT, h_unix_ms),
- (r_time_in_epoch_ms2, handler_time, QUERY_TYPE_DT, h_unix_ms),
- (r_time_in_epoch_ms3, handler_time, QUERY_TYPE_DT, h_unix_ms),
+ (r_time_in_epoch_ms2, handler_generic_parser, QUERY_TYPE_DT, h_unix_ms),
+ (r_time_in_epoch_ms3, handler_generic_parser, QUERY_TYPE_DT, h_unix_ms),
(r_timezone_translation, handler_timezone_translation, QUERY_TYPE_DT_TR, h_translation),
(r_timezone_translation_in_to, handler_timezone_translation_in_to, QUERY_TYPE_DT_TR, h_translation),
(r_time_since, handler_time_since_until, QUERY_TYPE_TD, h_default_td),
(r_time_until, handler_time_since_until, QUERY_TYPE_TD, h_default_td),
(r_time_between, handler_time_diff, QUERY_TYPE_TD, h_default_td),
+ (r_tz_between, handler_tz_diff, QUERY_TYPE_TD, h_default_td),
(r_time_plus, handler_time_plus, QUERY_TYPE_DT, h_default_dt),
(r_time_minus, handler_time_minus, QUERY_TYPE_DT, h_default_dt),
(r_time_before, handler_time_before, QUERY_TYPE_DT, h_default_dt),
@@ -226,6 +248,8 @@ regex_handlers = [
(r_timezone, handler_timezone, QUERY_TYPE_TZ, h_tz_offset),
(r_timezone_2, handler_timezone, QUERY_TYPE_TZ, h_tz_offset),
(r_hour_minute_timezone, handler_timezone_creation, QUERY_TYPE_DT_TR, h_translation),
+ (r_calendar_year, handler_calendar, QUERY_TYPE_CAL, h_cal_year),
+ (r_calendar_month, handler_calendar, QUERY_TYPE_CAL, h_cal_month),
(r_generic, handler_generic_parser, QUERY_TYPE_DT, h_default_dt),
]
@@ -351,7 +375,7 @@ def resolve_query(query, allowed_queries=None):
}
solutions = resolve_query_type(query)
if not allowed_queries:
- allowed_queries = [QUERY_TYPE_DT, QUERY_TYPE_DT_TR, QUERY_TYPE_TD, QUERY_TYPE_TZ]
+ allowed_queries = [QUERY_TYPE_DT, QUERY_TYPE_DT_TR, QUERY_TYPE_TD, QUERY_TYPE_TZ, QUERY_TYPE_CAL]
for sol_id, solution in enumerate(solutions):
element = {}
handler, results, query_type, hi = solution
@@ -372,6 +396,8 @@ def resolve_query(query, allowed_queries=None):
element["tz"] = results
elif query_type == QUERY_TYPE_TD:
element["timedelta"] = results
+ elif query_type == QUERY_TYPE_CAL:
+ element["cal"] = results
rv["solutions"].append(element)
except Exception as e:
continue
@@ -409,6 +435,7 @@ def parse_args():
parser = argparse.ArgumentParser()
parser.add_argument('query', nargs='*', default="", help="freeform")
parser.add_argument('--locale', dest='locale')
+ parser.add_argument('--tz', dest='tz', default='local')
parser.add_argument('--handlers', dest='handlers')
parser.add_argument('--show', dest='show')
parser.add_argument('--full', dest='full', action='store_true')
@@ -425,7 +452,9 @@ def setup_logging_level(debug=False):
def main(args):
global custom_locale
+ global custom_tz
custom_locale = resolve_locale(args.locale)
+ custom_tz = resolve_timezone(args.tz)
if custom_locale:
logger.debug("Locale understood as: {}".format(custom_locale))
query = ' '.join(args.query)