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)