workdays and hours calculated
This commit is contained in:
parent
ea18b83628
commit
d78a50c7d7
@ -1,34 +1,39 @@
|
||||
backcall==0.1.0
|
||||
chardet==3.0.4
|
||||
Click==7.0
|
||||
dateparser==0.7.2
|
||||
decorator==4.4.0
|
||||
DateTimeRange==0.6.1
|
||||
decorator==4.4.1
|
||||
Flask==1.1.1
|
||||
freezegun==0.3.12
|
||||
freezegun==0.3.14
|
||||
fuzzywuzzy==0.17.0
|
||||
geographiclib==1.49
|
||||
geopy==1.20.0
|
||||
geographiclib==1.50
|
||||
geopy==1.21.0
|
||||
importlib-resources==1.0.2
|
||||
ipython==7.8.0
|
||||
ipython==7.12.0
|
||||
ipython-genutils==0.2.0
|
||||
isodate==0.6.0
|
||||
itsdangerous==1.1.0
|
||||
jedi==0.15.1
|
||||
Jinja2==2.10.3
|
||||
jedi==0.16.0
|
||||
Jinja2==2.11.1
|
||||
MarkupSafe==1.1.1
|
||||
mbstrdecoder==0.8.4
|
||||
numpy==1.18.1
|
||||
parso==0.5.1
|
||||
pexpect==4.7.0
|
||||
parso==0.6.1
|
||||
pexpect==4.8.0
|
||||
pickleshare==0.7.5
|
||||
prompt-toolkit==2.0.9
|
||||
prompt-toolkit==3.0.3
|
||||
ptyprocess==0.6.0
|
||||
Pygments==2.4.2
|
||||
Pygments==2.5.2
|
||||
python-dateutil==2.8.1
|
||||
python-Levenshtein==0.12.0
|
||||
pytz==2019.3
|
||||
regex==2019.11.1
|
||||
regex==2020.1.8
|
||||
six==1.14.0
|
||||
timezonefinder==4.1.0
|
||||
traitlets==4.3.2
|
||||
timezonefinder==4.2.0
|
||||
traitlets==4.3.3
|
||||
typepy==0.6.5
|
||||
tzlocal==2.0.0
|
||||
wcwidth==0.1.7
|
||||
Werkzeug==0.16.0
|
||||
wcwidth==0.1.8
|
||||
Werkzeug==1.0.0
|
||||
word2number==1.1
|
||||
|
129
src/tww/localization.py
Normal file
129
src/tww/localization.py
Normal file
@ -0,0 +1,129 @@
|
||||
import contextlib
|
||||
import csv
|
||||
import locale
|
||||
import os
|
||||
|
||||
from fuzzywuzzy import fuzz
|
||||
|
||||
from tww import basepath
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def setlocale(*args, **kw):
|
||||
saved = locale.setlocale(locale.LC_ALL)
|
||||
yield locale.setlocale(*args, **kw)
|
||||
locale.setlocale(locale.LC_ALL, saved)
|
||||
|
||||
|
||||
def find_country_alias(locale_s):
|
||||
with open(os.path.join(basepath, "data", "country_codes.csv")) as f:
|
||||
cfile = csv.reader(f)
|
||||
for row in cfile:
|
||||
country, alpha2, alpha3 = row[0:3]
|
||||
country, alpha2, alpha3 = country.lower(), alpha2.lower(), alpha3.lower()
|
||||
if locale_s in [country, alpha2, alpha3]:
|
||||
return country, alpha2, alpha3
|
||||
fuzz_ratio = fuzz.ratio(locale_s, country)
|
||||
if fuzz_ratio > 90:
|
||||
return country, alpha2, alpha3
|
||||
return None, None, None
|
||||
|
||||
|
||||
def find_language_alias(locale_s):
|
||||
with open(os.path.join(basepath, "data", "language_codes.csv")) as f:
|
||||
cfile = csv.reader(f)
|
||||
for row in cfile:
|
||||
name, native_name, a2, a3 = row[1:5]
|
||||
name, native_name, a2, a3 = name.lower(), native_name.lower(), a2.lower(), a3.lower()
|
||||
if locale_s in [a2, a3, name, native_name]:
|
||||
return name, native_name, a2, a3
|
||||
fuzz_ratio = fuzz.ratio(locale_s, name)
|
||||
if fuzz_ratio > 90:
|
||||
return name, native_name, a2, a3
|
||||
fuzz_ratio = fuzz.ratio(locale_s, native_name)
|
||||
if fuzz_ratio > 80:
|
||||
return name, native_name, a2, a3
|
||||
return None, None, None, None
|
||||
|
||||
|
||||
def lc_time_to_codes(lc_time):
|
||||
country_lang, encoding = lc_time.split('.')
|
||||
country_code, lang_code = country_lang.split('_')
|
||||
return country_code, lang_code, encoding
|
||||
|
||||
|
||||
def get_default_locale():
|
||||
default_locale = locale.getlocale()
|
||||
if type(default_locale) == tuple:
|
||||
default_locale = "{}.{}".format(*default_locale)
|
||||
country_code, lang_code, encoding = lc_time_to_codes(default_locale)
|
||||
return country_code, lang_code, encoding, default_locale
|
||||
|
||||
|
||||
def resolve_locale(locale_s):
|
||||
country_code, lang_code, encoding, default_locale = get_default_locale()
|
||||
rv = dict(
|
||||
query=locale_s,
|
||||
country_code=country_code,
|
||||
lang_code=lang_code,
|
||||
encoding=encoding,
|
||||
lc_time=default_locale,
|
||||
)
|
||||
default_encoding = 'utf-8'
|
||||
if not locale_s:
|
||||
return rv
|
||||
if '.' in locale_s:
|
||||
country_lang, encoding = locale_s.split('.')
|
||||
else:
|
||||
country_lang, encoding = locale_s, default_encoding
|
||||
if '_' in country_lang:
|
||||
country_code, lang_code = country_lang.split('_')
|
||||
if len(country_code) == 2 and len(lang_code) == 2:
|
||||
try:
|
||||
lc_time = "{}_{}.{}".format(country_code, lang_code, encoding)
|
||||
locale.setlocale(locale.LC_TIME, lc_time)
|
||||
rv["country_code"] = country_code
|
||||
rv["lang_code"] = lang_code
|
||||
rv["encoding"] = encoding
|
||||
rv["lc_time"] = lc_time
|
||||
return rv
|
||||
except:
|
||||
...
|
||||
locale_s = locale_s.strip().lower()
|
||||
country, alpha2, alpha3 = find_country_alias(locale_s)
|
||||
lang_name, lang_native_name, lang2, lang3 = find_language_alias(locale_s)
|
||||
if alpha2:
|
||||
locale_hypotheses = {k: v for k, v in locale.locale_alias.items() if k.startswith(alpha2)}
|
||||
for k, v in locale_hypotheses.items():
|
||||
lower = k.lower()
|
||||
if 'utf-8' in lower:
|
||||
rv["lc_time"] = v
|
||||
break
|
||||
else:
|
||||
if locale_hypotheses:
|
||||
lc_time = locale_hypotheses.get(alpha2)
|
||||
if lc_time:
|
||||
country_code, lang_code, encoding = lc_time_to_codes(lc_time)
|
||||
rv["country_code"] = country_code
|
||||
rv["lang_code"] = lang_code
|
||||
rv["encoding"] = encoding
|
||||
rv["lc_time"] = lc_time
|
||||
return rv
|
||||
if lang2:
|
||||
locale_hypotheses = {k: v for k, v in locale.locale_alias.items() if k.startswith(lang2)}
|
||||
for k, v in locale_hypotheses.items():
|
||||
lower = k.lower()
|
||||
if 'utf-8' in lower:
|
||||
rv["lc_time"] = v
|
||||
break
|
||||
else:
|
||||
if locale_hypotheses:
|
||||
lc_time = locale_hypotheses.get(lang2)
|
||||
if lc_time:
|
||||
country_code, lang_code, encoding = lc_time_to_codes(lc_time)
|
||||
rv["country_code"] = country_code
|
||||
rv["lang_code"] = lang_code
|
||||
rv["encoding"] = encoding
|
||||
rv["lc_time"] = lc_time
|
||||
return rv
|
||||
return rv
|
@ -1,16 +1,13 @@
|
||||
import contextlib
|
||||
import csv
|
||||
import json
|
||||
import locale
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
from fuzzywuzzy import fuzz
|
||||
from pygments import highlight, lexers, formatters
|
||||
|
||||
from tww import ISO_FORMAT, time_to_emoji, time_ago, basepath
|
||||
from localization import setlocale, resolve_locale
|
||||
from tww import ISO_FORMAT, time_to_emoji, time_ago, workday_diff, workhours_diff, td_remainders, td_totals, td_iso8601
|
||||
from tww import resolve_timezone, dateparser_parse_dt, get_utcnow, get_s_since_epoch, get_ms_since_epoch, \
|
||||
dt_tz_translation, get_local_now, query_to_format_result
|
||||
|
||||
@ -25,6 +22,12 @@ r_time_in = re.compile('(?:time)?\s*in\s*(.*)', flags=re.IGNORECASE)
|
||||
r_time_since = re.compile('(?:time)?\s*since\s*(.*)', flags=re.IGNORECASE)
|
||||
r_time_until = re.compile('(?:time)?\s*until\s*(.*)', flags=re.IGNORECASE)
|
||||
r_time_between = re.compile('(?:time)?\s*between\s*(.*)\s*and\s*(.*)', flags=re.IGNORECASE)
|
||||
r_workdays_since = re.compile('(?:workdays|work days)?\s*since\s*(.*)', flags=re.IGNORECASE)
|
||||
r_workdays_until = re.compile('(?:workdays|work days)?\s*until\s*(.*)', flags=re.IGNORECASE)
|
||||
r_workdays_between = re.compile('(?:workdays|work days)?\s*between\s*(.*)\s*and\s*(.*)', flags=re.IGNORECASE)
|
||||
r_workhours_since = re.compile('(?:workhours|work hours)?\s*since\s*(.*)', flags=re.IGNORECASE)
|
||||
r_workhours_until = re.compile('(?:workhours|work hours)?\s*until\s*(.*)', flags=re.IGNORECASE)
|
||||
r_workhours_between = re.compile('(?:workhours|work hours)?\s*between\s*(.*)\s*and\s*(.*)', flags=re.IGNORECASE)
|
||||
r_timezone_translation = re.compile('(.*)?\s(?:in|to)\s(.*)', flags=re.IGNORECASE)
|
||||
r_timezone = re.compile('(.*)?\s(?:timezone|timezones|tz)', flags=re.IGNORECASE)
|
||||
r_timezone_2 = re.compile('(?:timezone in|timezones in|tz in|timezone|timezones|tz)\s(.*)?', flags=re.IGNORECASE)
|
||||
@ -65,6 +68,28 @@ def handler_time_since_until(start_dt_s: str) -> dict:
|
||||
return handler_time_diff(dateparser_parse_dt(start_dt_s), get_local_now())
|
||||
|
||||
|
||||
def handler_workdays_diff(start_dt: datetime, end_dt: datetime) -> dict:
|
||||
diff = workday_diff(start_dt, end_dt)
|
||||
return dict(start=dt_pretty(start_dt),
|
||||
end=dt_pretty(end_dt),
|
||||
diff=td_pretty(diff))
|
||||
|
||||
|
||||
def handler_workdays_since_until(start_dt_s: str) -> dict:
|
||||
return handler_workdays_diff(dateparser_parse_dt(start_dt_s), get_local_now())
|
||||
|
||||
|
||||
def handler_workhours_diff(start_dt: datetime, end_dt: datetime) -> dict:
|
||||
diff = workhours_diff(start_dt, end_dt)
|
||||
return dict(start=dt_pretty(start_dt),
|
||||
end=dt_pretty(end_dt),
|
||||
diff=td_pretty(diff))
|
||||
|
||||
|
||||
def handler_workhours_since_until(start_dt_s: str) -> dict:
|
||||
return handler_workhours_diff(dateparser_parse_dt(start_dt_s), get_local_now())
|
||||
|
||||
|
||||
def handler_timezone_translation(dt_s: str, timezone_like_s: str) -> dict:
|
||||
src_dt = dateparser_parse_dt(dt_s)
|
||||
tz = resolve_timezone(timezone_like_s)
|
||||
@ -106,6 +131,12 @@ regex_handlers = [
|
||||
(r_time_since, handler_time_since_until, QUERY_TYPE_TD),
|
||||
(r_time_until, handler_time_since_until, QUERY_TYPE_TD),
|
||||
(r_time_between, handler_time_diff, QUERY_TYPE_TD),
|
||||
(r_workdays_since, handler_workdays_since_until, QUERY_TYPE_TD),
|
||||
(r_workdays_until, handler_workdays_since_until, QUERY_TYPE_TD),
|
||||
(r_workdays_between, handler_workdays_diff, QUERY_TYPE_TD),
|
||||
(r_workhours_since, handler_workhours_since_until, QUERY_TYPE_TD),
|
||||
(r_workhours_until, handler_workhours_since_until, QUERY_TYPE_TD),
|
||||
(r_workhours_between, handler_workhours_diff, QUERY_TYPE_TD),
|
||||
(r_time_in, handler_time_in_parser, QUERY_TYPE_DT),
|
||||
(r_timezone, handler_timezone, QUERY_TYPE_TZ),
|
||||
(r_timezone_2, handler_timezone, QUERY_TYPE_TZ),
|
||||
@ -137,158 +168,12 @@ def tokenize(s):
|
||||
return solutions
|
||||
|
||||
|
||||
def test():
|
||||
test_strings = [
|
||||
None,
|
||||
"",
|
||||
"s",
|
||||
" ",
|
||||
"Time since 2019-05-12",
|
||||
"Since yesterday",
|
||||
"time between yesterday and tomorrow",
|
||||
"time until 25 december",
|
||||
"time sinc",
|
||||
"now in milliseconds",
|
||||
"seconds since epoch",
|
||||
"1992-01-27 to epoch",
|
||||
"milliseconds since 1992-01-27",
|
||||
"now in sofia",
|
||||
"now in PST",
|
||||
"2 hours ago to Sydney",
|
||||
"now in +03:00",
|
||||
"now in dublin",
|
||||
]
|
||||
for s in test_strings:
|
||||
print("{} -> {}".format(s, tokenize(s)))
|
||||
|
||||
|
||||
def pretty_print_dict(obj):
|
||||
formatted_json = json.dumps(obj, indent=2, ensure_ascii=False)
|
||||
colorful_json = highlight(formatted_json, lexers.JsonLexer(), formatters.TerminalFormatter())
|
||||
print(colorful_json)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def setlocale(*args, **kw):
|
||||
saved = locale.setlocale(locale.LC_ALL)
|
||||
yield locale.setlocale(*args, **kw)
|
||||
locale.setlocale(locale.LC_ALL, saved)
|
||||
|
||||
|
||||
def find_country_alias(locale_s):
|
||||
with open(os.path.join(basepath, "data", "country_codes.csv")) as f:
|
||||
cfile = csv.reader(f)
|
||||
for row in cfile:
|
||||
country, alpha2, alpha3 = row[0:3]
|
||||
country, alpha2, alpha3 = country.lower(), alpha2.lower(), alpha3.lower()
|
||||
if locale_s in [country, alpha2, alpha3]:
|
||||
return country, alpha2, alpha3
|
||||
fuzz_ratio = fuzz.ratio(locale_s, country)
|
||||
if fuzz_ratio > 90:
|
||||
return country, alpha2, alpha3
|
||||
return None, None, None
|
||||
|
||||
|
||||
def find_language_alias(locale_s):
|
||||
with open(os.path.join(basepath, "data", "language_codes.csv")) as f:
|
||||
cfile = csv.reader(f)
|
||||
for row in cfile:
|
||||
name, native_name, a2, a3 = row[1:5]
|
||||
name, native_name, a2, a3 = name.lower(), native_name.lower(), a2.lower(), a3.lower()
|
||||
if locale_s in [a2, a3, name, native_name]:
|
||||
return name, native_name, a2, a3
|
||||
fuzz_ratio = fuzz.ratio(locale_s, name)
|
||||
if fuzz_ratio > 90:
|
||||
return name, native_name, a2, a3
|
||||
fuzz_ratio = fuzz.ratio(locale_s, native_name)
|
||||
if fuzz_ratio > 80:
|
||||
return name, native_name, a2, a3
|
||||
return None, None, None, None
|
||||
|
||||
|
||||
def lc_time_to_codes(lc_time):
|
||||
country_lang, encoding = lc_time.split('.')
|
||||
country_code, lang_code = country_lang.split('_')
|
||||
return country_code, lang_code, encoding
|
||||
|
||||
|
||||
def get_default_locale():
|
||||
default_locale = locale.getlocale()
|
||||
if type(default_locale) == tuple:
|
||||
default_locale = "{}.{}".format(*default_locale)
|
||||
country_code, lang_code, encoding = lc_time_to_codes(default_locale)
|
||||
return country_code, lang_code, encoding, default_locale
|
||||
|
||||
|
||||
def resolve_locale(locale_s):
|
||||
country_code, lang_code, encoding, default_locale = get_default_locale()
|
||||
rv = dict(
|
||||
query=locale_s,
|
||||
country_code=country_code,
|
||||
lang_code=lang_code,
|
||||
encoding=encoding,
|
||||
lc_time=default_locale,
|
||||
)
|
||||
default_encoding = 'utf-8'
|
||||
if not locale_s:
|
||||
return rv
|
||||
if '.' in locale_s:
|
||||
country_lang, encoding = locale_s.split('.')
|
||||
else:
|
||||
country_lang, encoding = locale_s, default_encoding
|
||||
if '_' in country_lang:
|
||||
country_code, lang_code = country_lang.split('_')
|
||||
if len(country_code) == 2 and len(lang_code) == 2:
|
||||
try:
|
||||
lc_time = "{}_{}.{}".format(country_code, lang_code, encoding)
|
||||
locale.setlocale(locale.LC_TIME, lc_time)
|
||||
rv["country_code"] = country_code
|
||||
rv["lang_code"] = lang_code
|
||||
rv["encoding"] = encoding
|
||||
rv["lc_time"] = lc_time
|
||||
return rv
|
||||
except:
|
||||
...
|
||||
locale_s = locale_s.strip().lower()
|
||||
country, alpha2, alpha3 = find_country_alias(locale_s)
|
||||
lang_name, lang_native_name, lang2, lang3 = find_language_alias(locale_s)
|
||||
if alpha2:
|
||||
locale_hypotheses = {k: v for k, v in locale.locale_alias.items() if k.startswith(alpha2)}
|
||||
for k, v in locale_hypotheses.items():
|
||||
lower = k.lower()
|
||||
if 'utf-8' in lower:
|
||||
rv["lc_time"] = v
|
||||
break
|
||||
else:
|
||||
if locale_hypotheses:
|
||||
lc_time = locale_hypotheses.get(alpha2)
|
||||
if lc_time:
|
||||
country_code, lang_code, encoding = lc_time_to_codes(lc_time)
|
||||
rv["country_code"] = country_code
|
||||
rv["lang_code"] = lang_code
|
||||
rv["encoding"] = encoding
|
||||
rv["lc_time"] = lc_time
|
||||
return rv
|
||||
if lang2:
|
||||
locale_hypotheses = {k: v for k, v in locale.locale_alias.items() if k.startswith(lang2)}
|
||||
for k, v in locale_hypotheses.items():
|
||||
lower = k.lower()
|
||||
if 'utf-8' in lower:
|
||||
rv["lc_time"] = v
|
||||
break
|
||||
else:
|
||||
if locale_hypotheses:
|
||||
lc_time = locale_hypotheses.get(lang2)
|
||||
if lc_time:
|
||||
country_code, lang_code, encoding = lc_time_to_codes(lc_time)
|
||||
rv["country_code"] = country_code
|
||||
rv["lang_code"] = lang_code
|
||||
rv["encoding"] = encoding
|
||||
rv["lc_time"] = lc_time
|
||||
return rv
|
||||
return rv
|
||||
|
||||
|
||||
def dt_pretty(dt):
|
||||
rv = {}
|
||||
global custom_locale
|
||||
@ -313,63 +198,6 @@ def dt_pretty(dt):
|
||||
return rv
|
||||
|
||||
|
||||
def td_remainders(td):
|
||||
# split seconds to larger units
|
||||
seconds = td.total_seconds()
|
||||
minutes, seconds = divmod(seconds, 60)
|
||||
hours, minutes = divmod(minutes, 60)
|
||||
days, hours = divmod(hours, 24)
|
||||
months, days = divmod(days, 30.42)
|
||||
years, months = divmod(months, 12)
|
||||
years, months, days, hours, minutes, seconds = map(int, (years, months, days, hours, minutes, seconds))
|
||||
years, months, days, hours, minutes, seconds = map(abs, (years, months, days, hours, minutes, seconds))
|
||||
return dict(
|
||||
seconds=seconds,
|
||||
minutes=minutes,
|
||||
hours=hours,
|
||||
days=days,
|
||||
months=months,
|
||||
years=years,
|
||||
)
|
||||
|
||||
|
||||
def td_totals(td):
|
||||
seconds = td.total_seconds()
|
||||
minutes = seconds // 60
|
||||
hours = seconds // (60 * 60)
|
||||
days = seconds // (24 * 60 * 60)
|
||||
weeks = seconds // (7 * 24 * 60 * 60)
|
||||
months = seconds // (30 * 24 * 60 * 60)
|
||||
years = seconds // (365 * 24 * 60 * 60)
|
||||
years, months, weeks, days, hours, minutes, seconds = map(abs,
|
||||
(years, months, weeks, days, hours, minutes, seconds))
|
||||
return dict(
|
||||
seconds=seconds,
|
||||
minutes=minutes,
|
||||
hours=hours,
|
||||
days=days,
|
||||
weeks=weeks,
|
||||
months=months,
|
||||
years=years,
|
||||
)
|
||||
|
||||
|
||||
def td_iso8601(td):
|
||||
"""P[n]Y[n]M[n]DT[n]H[n]M[n]S"""
|
||||
rem = td_remainders(td)
|
||||
fmt = "P"
|
||||
for short, timeframe in [("Y", "years"), ("M", "months"), ("D", "days")]:
|
||||
if rem[timeframe]:
|
||||
fmt += "{}{}".format(rem[timeframe], short)
|
||||
hms = [("H", "hours"), ("M", "minutes"), ("S", "seconds")]
|
||||
if any([rem[t[1]] for t in hms]):
|
||||
fmt += "T"
|
||||
for short, timeframe in hms:
|
||||
if rem[timeframe]:
|
||||
fmt += "{}{}".format(rem[timeframe], short)
|
||||
return fmt
|
||||
|
||||
|
||||
def td_pretty(td):
|
||||
rv = {
|
||||
"sign": '-' if td.days < 0 else '+',
|
||||
@ -401,6 +229,7 @@ def resolve_query(query):
|
||||
handler, results, query_type = solution
|
||||
element["handler"] = handler
|
||||
element["query_type"] = query_type
|
||||
try:
|
||||
if query_type == QUERY_TYPE_DT:
|
||||
element["dt"] = dt_pretty(results)
|
||||
elif query_type == QUERY_TYPE_DT_TR:
|
||||
@ -412,13 +241,42 @@ def resolve_query(query):
|
||||
elif query_type == QUERY_TYPE_TD:
|
||||
element["timedelta"] = results
|
||||
rv["solutions"].append(element)
|
||||
except Exception:
|
||||
...
|
||||
return rv
|
||||
|
||||
|
||||
def test():
|
||||
test_strings = [
|
||||
None,
|
||||
"",
|
||||
"s",
|
||||
" ",
|
||||
"Time since 2019-05-12",
|
||||
"Since yesterday",
|
||||
"time between yesterday and tomorrow",
|
||||
"time until 25 december",
|
||||
"time sinc",
|
||||
"now in milliseconds",
|
||||
"seconds since epoch",
|
||||
"1992-01-27 to epoch",
|
||||
"milliseconds since 1992-01-27",
|
||||
"now in sofia",
|
||||
"now in PST",
|
||||
"2 hours ago to Sydney",
|
||||
"now in +03:00",
|
||||
"now in dublin",
|
||||
"workdays since 2/07/2020 12:00",
|
||||
"workhours since 2/07/2020 12:00",
|
||||
]
|
||||
for s in test_strings:
|
||||
print("{} -> {}".format(s, resolve_query(s)))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
query = ' '.join(sys.argv[1:])
|
||||
# query = "now in india"
|
||||
custom_locale = ""
|
||||
custom_locale = resolve_locale(custom_locale)
|
||||
query = ' '.join(sys.argv[1:])
|
||||
# query = "workhours until 2/12/2020 12:00"
|
||||
result = resolve_query(query)
|
||||
pretty_print_dict(result)
|
||||
|
128
src/tww/tww.py
128
src/tww/tww.py
@ -14,6 +14,7 @@ from datetime import datetime, timedelta
|
||||
|
||||
import pytz
|
||||
from dateparser import parse as parse_dt
|
||||
from datetimerange import DateTimeRange
|
||||
from dateutil.parser import parse as dutil_parse
|
||||
from dateparser.timezone_parser import StaticTzInfo
|
||||
from dateutil.tz import gettz, tzlocal
|
||||
@ -183,7 +184,7 @@ def resolve_location_local(query):
|
||||
try:
|
||||
result = heappop(heap)
|
||||
except IndexError:
|
||||
logger.error("Could not find location {}".format(query))
|
||||
logger.info("Could not find location {}".format(query))
|
||||
return ""
|
||||
ratio, location = result
|
||||
logger.debug("Location result ({}): {}".format(-ratio, location))
|
||||
@ -205,17 +206,18 @@ def resolve_location_remote(query):
|
||||
write_to_cache(query, location)
|
||||
return location
|
||||
except GeocoderTimedOut:
|
||||
logger.error("Timed out resolving location. Try specifying a timezone directly")
|
||||
logger.info("Timed out resolving location. Try specifying a timezone directly")
|
||||
|
||||
|
||||
def parse_query(query):
|
||||
"""
|
||||
TODO: DEPRECATE THIS
|
||||
Parses the user query to the datetime, tz/loc parts
|
||||
"""
|
||||
# query = ' '.join(query)
|
||||
query = query.strip()
|
||||
if not query:
|
||||
logger.error("Use a query like <datetime-like> ['to' <timezone or location>]")
|
||||
logger.info("TO DEPRECATE: Use a query like <datetime-like> ['to' <timezone or location>]")
|
||||
to_query = query.split(" to ")
|
||||
logger.debug("to_query: {}".format(to_query))
|
||||
if len(to_query) == 1:
|
||||
@ -225,7 +227,7 @@ def parse_query(query):
|
||||
# datetime to timezone
|
||||
human_dt, human_tz_loc = to_query
|
||||
else:
|
||||
logger.error("There can be only one 'to' in the query string")
|
||||
logger.info("TO DEPRECATE: There can be only one 'to' in the query string")
|
||||
|
||||
logger.debug("raw human_dt: {}".format(human_dt))
|
||||
logger.debug("raw human_tz_loc: {}".format(human_tz_loc))
|
||||
@ -358,7 +360,7 @@ def solve_query(human_dt, human_tz_loc):
|
||||
|
||||
def format_result(result, fmt):
|
||||
if result is None:
|
||||
logger.error("Could not solve query")
|
||||
logger.info("Could not solve query")
|
||||
logger.debug("Format: {}".format(fmt))
|
||||
format_result = result.strftime(fmt)
|
||||
logger.debug("Formated result: {} -> {}".format(result, format_result))
|
||||
@ -577,3 +579,119 @@ def time_to_emoji(dt):
|
||||
seconds = get_local_s_since_epoch(dt)
|
||||
a = int((seconds / 900 - 3) / 2 % 24)
|
||||
return chr(128336 + a // 2 + a % 2 * 12)
|
||||
|
||||
|
||||
def workday_diff(start, end, workdays=None):
|
||||
"""
|
||||
Calculates the difference between two dates excluding weekends
|
||||
|
||||
# TODO: doesn't work with Until (i.e. future calculation)
|
||||
"""
|
||||
if not workdays:
|
||||
workdays = range(0, 5)
|
||||
td = end - start
|
||||
daygenerator = (start + timedelta(x + 1) for x in range(td.days))
|
||||
weekdays = sum(1 for day in daygenerator if day.weekday() in workdays)
|
||||
return timedelta(days=weekdays)
|
||||
|
||||
|
||||
def workhours_diff(start, end, workhour_begin="09:00", workhour_end="17:00", workdays=None):
|
||||
"""
|
||||
Calculates the difference between two dates excluding non-workhours
|
||||
This can potentially be very slow for long ranges as it calculates per minute resolution.
|
||||
|
||||
# TODO: doesn't work with Until (i.e. future calculation)
|
||||
|
||||
"""
|
||||
if not workdays:
|
||||
workdays = range(0, 5)
|
||||
|
||||
workday_start_h, workday_start_m = map(int, workhour_begin.split(':'))
|
||||
workday_end_h, workday_end_m = map(int, workhour_end.split(':'))
|
||||
|
||||
# assume night shift if next workday starts after
|
||||
day_diff = 1 if workday_end_h < workday_start_h else 0
|
||||
|
||||
prev_dt_minute, dt_minute = start, start + timedelta(minutes=1)
|
||||
summins = 0
|
||||
while dt_minute < end:
|
||||
if dt_minute.weekday() not in workdays:
|
||||
prev_dt_minute, dt_minute = prev_dt_minute + timedelta(days=1), dt_minute + timedelta(days=1)
|
||||
continue
|
||||
this_day_workhours_begin = datetime(year=dt_minute.year, month=dt_minute.month, day=dt_minute.day,
|
||||
hour=workday_start_h, minute=workday_start_m, tzinfo=dt_minute.tzinfo)
|
||||
this_day_workhours_end = datetime(year=dt_minute.year, month=dt_minute.month, day=dt_minute.day,
|
||||
hour=workday_end_h, minute=workday_end_m, tzinfo=dt_minute.tzinfo)
|
||||
# calc if night shift
|
||||
this_day_workhours_end += timedelta(days=day_diff)
|
||||
|
||||
# test if this minute is within workhours with daterange
|
||||
this_day_workhours = DateTimeRange(this_day_workhours_begin, this_day_workhours_end)
|
||||
time_range = DateTimeRange(prev_dt_minute, dt_minute)
|
||||
if time_range in this_day_workhours:
|
||||
# we are in workhours, add all the minutes here until the end (now) or end of workday - whichever is smaller
|
||||
end_delta = end if end < this_day_workhours_end else this_day_workhours_end
|
||||
summins += (end_delta - prev_dt_minute).total_seconds() // 60
|
||||
prev_dt_minute = end_delta
|
||||
else:
|
||||
# skip until next workday - naively add one day; it could be weekend, but it will be caught above
|
||||
prev_dt_minute = this_day_workhours_begin + timedelta(days=1)
|
||||
dt_minute = prev_dt_minute + timedelta(minutes=1)
|
||||
return timedelta(seconds=int(summins * 60))
|
||||
|
||||
|
||||
def td_remainders(td):
|
||||
# split seconds to larger units
|
||||
seconds = td.total_seconds()
|
||||
minutes, seconds = divmod(seconds, 60)
|
||||
hours, minutes = divmod(minutes, 60)
|
||||
days, hours = divmod(hours, 24)
|
||||
months, days = divmod(days, 30.42)
|
||||
years, months = divmod(months, 12)
|
||||
years, months, days, hours, minutes, seconds = map(int, (years, months, days, hours, minutes, seconds))
|
||||
years, months, days, hours, minutes, seconds = map(abs, (years, months, days, hours, minutes, seconds))
|
||||
return dict(
|
||||
seconds=seconds,
|
||||
minutes=minutes,
|
||||
hours=hours,
|
||||
days=days,
|
||||
months=months,
|
||||
years=years,
|
||||
)
|
||||
|
||||
|
||||
def td_totals(td):
|
||||
seconds = td.total_seconds()
|
||||
minutes = seconds // 60
|
||||
hours = seconds // (60 * 60)
|
||||
days = seconds // (24 * 60 * 60)
|
||||
weeks = seconds // (7 * 24 * 60 * 60)
|
||||
months = seconds // (30 * 24 * 60 * 60)
|
||||
years = seconds // (365 * 24 * 60 * 60)
|
||||
years, months, weeks, days, hours, minutes, seconds = map(abs,
|
||||
(years, months, weeks, days, hours, minutes, seconds))
|
||||
return dict(
|
||||
seconds=seconds,
|
||||
minutes=minutes,
|
||||
hours=hours,
|
||||
days=days,
|
||||
weeks=weeks,
|
||||
months=months,
|
||||
years=years,
|
||||
)
|
||||
|
||||
|
||||
def td_iso8601(td):
|
||||
"""P[n]Y[n]M[n]DT[n]H[n]M[n]S"""
|
||||
rem = td_remainders(td)
|
||||
fmt = "P"
|
||||
for short, timeframe in [("Y", "years"), ("M", "months"), ("D", "days")]:
|
||||
if rem[timeframe]:
|
||||
fmt += "{}{}".format(rem[timeframe], short)
|
||||
hms = [("H", "hours"), ("M", "minutes"), ("S", "seconds")]
|
||||
if any([rem[t[1]] for t in hms]):
|
||||
fmt += "T"
|
||||
for short, timeframe in hms:
|
||||
if rem[timeframe]:
|
||||
fmt += "{}{}".format(rem[timeframe], short)
|
||||
return fmt
|
Loading…
Reference in New Issue
Block a user