summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--conversion_utils.py160
-rwxr-xr-xdateparse/dateparse_utils.py10
-rw-r--r--datetime_utils.py350
-rw-r--r--executors.py2
-rw-r--r--text_utils.py22
-rw-r--r--type/centcount.py3
-rw-r--r--unittest_utils.py32
7 files changed, 530 insertions, 49 deletions
diff --git a/conversion_utils.py b/conversion_utils.py
index b83d62e..d2225fd 100644
--- a/conversion_utils.py
+++ b/conversion_utils.py
@@ -7,11 +7,27 @@ import constants
class Converter(object):
+ """A converter has a canonical name and a category. The name defines
+ a unit of measurement in a category or class of measurements.
+ This framework will allow conversion between named units in the
+ same category. e.g. name may be "meter", "inch", "mile" and their
+ category may be "length".
+
+ The way that conversions work is that we convert the magnitude
+ first to a canonical unit and then (if needed) from the canonical
+ unit to the desired destination unit. e.g. if "meter" is the
+ canonical unit of measurement for the category "length", in order
+ to convert miles into inches we first convert miles into meters
+ and, from there, meters into inches. This is potentially
+ dangerous because it requires two floating point operations which
+ each have the potential to overflow, underflow, or introduce
+ floating point errors. Caveat emptor.
+ """
def __init__(self,
name: str,
category: str,
- to_canonical: Callable,
- from_canonical: Callable,
+ to_canonical: Callable, # convert to canonical unit
+ from_canonical: Callable, # convert from canonical unit
unit: str) -> None:
self.name = name
self.category = category
@@ -29,6 +45,7 @@ class Converter(object):
return self.unit
+# A catalog of converters.
conversion_catalog = {
"Second": Converter("Second",
"time",
@@ -94,90 +111,225 @@ def _convert(magnitude: Number,
def sec_to_min(s: float) -> float:
+ """
+ Convert seconds into minutes.
+
+ >>> sec_to_min(120)
+ 2.0
+ """
return convert(s, "Second", "Minute")
def sec_to_hour(s: float) -> float:
+ """
+ Convert seconds into hours.
+
+ >>> sec_to_hour(1800)
+ 0.5
+ """
return convert(s, "Second", "Hour")
def sec_to_day(s: float) -> float:
+ """
+ Convert seconds into days.
+
+ >>> sec_to_day(1800)
+ 0.020833333333333332
+ """
return convert(s, "Second", "Day")
def sec_to_week(s: float) -> float:
+ """
+ Convert seconds into weeks.
+
+ >>> sec_to_week(1800)
+ 0.002976190476190476
+ """
return convert(s, "Second", "Week")
def min_to_sec(m: float) -> float:
+ """
+ Convert minutes into seconds.
+
+ >>> min_to_sec(5)
+ 300.0
+ """
return convert(m, "Minute", "Second")
def min_to_hour(m: float) -> float:
+ """
+ Convert minutes into hours.
+
+ >>> min_to_hour(120)
+ 2.0
+ """
return convert(m, "Minute", "Hour")
def min_to_day(m: float) -> float:
+ """
+ Convert minutes into days.
+
+ >>> min_to_day(60 * 12)
+ 0.5
+ """
return convert(m, "Minute", "Day")
def min_to_week(m: float) -> float:
+ """
+ Convert minutes into weeks.
+
+ >>> min_to_week(60 * 24 * 3)
+ 0.42857142857142855
+ """
return convert(m, "Minute", "Week")
def hour_to_sec(h: float) -> float:
+ """
+ Convert hours into seconds.
+
+ >>> hour_to_sec(1)
+ 3600.0
+ """
return convert(h, "Hour", "Second")
def hour_to_min(h: float) -> float:
+ """
+ Convert hours into minutes.
+
+ >>> hour_to_min(2)
+ 120.0
+ """
return convert(h, "Hour", "Minute")
def hour_to_day(h: float) -> float:
+ """
+ Convert hours into days.
+
+ >>> hour_to_day(36)
+ 1.5
+ """
return convert(h, "Hour", "Day")
def hour_to_week(h: float) -> float:
+ """
+ Convert hours into weeks.
+
+ >>> hour_to_week(24)
+ 0.14285714285714285
+ """
return convert(h, "Hour", "Week")
def day_to_sec(d: float) -> float:
+ """
+ Convert days into seconds.
+
+ >>> day_to_sec(1)
+ 86400.0
+ """
return convert(d, "Day", "Second")
def day_to_min(d: float) -> float:
+ """
+ Convert days into minutes.
+
+ >>> day_to_min(0.1)
+ 144.0
+ """
return convert(d, "Day", "Minute")
def day_to_hour(d: float) -> float:
+ """
+ Convert days into hours.
+
+ >>> day_to_hour(1)
+ 24.0
+ """
return convert(d, "Day", "Hour")
def day_to_week(d: float) -> float:
+ """
+ Convert days into weeks.
+
+ >>> day_to_week(14)
+ 2.0
+ """
return convert(d, "Day", "Week")
def week_to_sec(w: float) -> float:
+ """
+ Convert weeks into seconds.
+
+ >>> week_to_sec(10)
+ 6048000.0
+ """
return convert(w, "Week", "Second")
def week_to_min(w: float) -> float:
+ """
+ Convert weeks into minutes.
+
+ >>> week_to_min(1)
+ 10080.0
+ """
return convert(w, "Week", "Minute")
def week_to_hour(w: float) -> float:
+ """
+ Convert weeks into hours.
+
+ >>> week_to_hour(1)
+ 168.0
+ """
return convert(w, "Week", "Hour")
def week_to_day(w: float) -> float:
+ """
+ Convert weeks into days.
+
+ >>> week_to_day(1)
+ 7.0
+ """
return convert(w, "Week", "Day")
def f_to_c(temp_f: float) -> float:
- """Fahrenheit to Celsius."""
+ """
+ Convert Fahrenheit into Celsius.
+
+ >>> f_to_c(32.0)
+ 0.0
+ """
return convert(temp_f, "Fahrenheit", "Celsius")
def c_to_f(temp_c: float) -> float:
- """Celsius to Fahrenheit."""
+ """
+ Convert Celsius to Fahrenheit.
+
+ >>> c_to_f(0.0)
+ 32.0
+ """
return convert(temp_c, "Celsius", "Fahrenheit")
+
+
+if __name__ == '__main__':
+ import doctest
+ doctest.testmod()
diff --git a/dateparse/dateparse_utils.py b/dateparse/dateparse_utils.py
index e5e7e76..f354ad0 100755
--- a/dateparse/dateparse_utils.py
+++ b/dateparse/dateparse_utils.py
@@ -271,9 +271,9 @@ class DateParser(dateparse_utilsListener):
return TimeUnit.MONTHS
txt = orig.lower()[:3]
if txt in self.day_name_to_number:
- return(self.day_name_to_number[txt])
+ return(TimeUnit(self.day_name_to_number[txt]))
elif txt in self.delta_unit_to_constant:
- return(self.delta_unit_to_constant[txt])
+ return(TimeUnit(self.delta_unit_to_constant[txt]))
raise ParseException(f'Invalid date unit: {orig}')
def _figure_out_time_unit(self, orig: str) -> int:
@@ -509,7 +509,7 @@ class DateParser(dateparse_utilsListener):
unit = self.context['delta_unit']
dt = n_timeunits_from_base(
count,
- unit,
+ TimeUnit(unit),
date_to_datetime(self.date)
)
self.date = datetime_to_date(dt)
@@ -890,7 +890,7 @@ class DateParser(dateparse_utilsListener):
unit = self._figure_out_date_unit(unit)
d = n_timeunits_from_base(
count,
- unit,
+ TimeUnit(unit),
d)
self.context['year'] = d.year
self.context['month'] = d.month
@@ -920,7 +920,7 @@ class DateParser(dateparse_utilsListener):
unit = self._figure_out_date_unit(unit)
d = n_timeunits_from_base(
count,
- unit,
+ TimeUnit(unit),
d)
self.context['year'] = d.year
self.context['month'] = d.month
diff --git a/datetime_utils.py b/datetime_utils.py
index 7787c6f..db5b2b5 100644
--- a/datetime_utils.py
+++ b/datetime_utils.py
@@ -18,18 +18,44 @@ logger = logging.getLogger(__name__)
def replace_timezone(dt: datetime.datetime,
tz: datetime.tzinfo) -> datetime.datetime:
+ """
+ Replaces the timezone on a datetime object.
+
+ >>> from pytz import UTC
+ >>> d = now_pacific()
+ >>> d.tzinfo.tzname(d)[0] # Note: could be PST or PDT
+ 'P'
+ >>> o = replace_timezone(d, UTC)
+ >>> o.tzinfo.tzname(o)
+ 'UTC'
+
+ """
return dt.replace(tzinfo=None).astimezone(tz=tz)
def now() -> datetime.datetime:
+ """
+ What time is it? Result returned in UTC
+ """
return datetime.datetime.now()
-def now_pst() -> datetime.datetime:
+def now_pacific() -> datetime.datetime:
+ """
+ What time is it? Result in US/Pacifit time (PST/PDT)
+ """
return replace_timezone(now(), pytz.timezone("US/Pacific"))
def date_to_datetime(date: datetime.date) -> datetime.datetime:
+ """
+ Given a date, return a datetime with hour/min/sec zero (midnight)
+
+ >>> import datetime
+ >>> date_to_datetime(datetime.date(2021, 12, 25))
+ datetime.datetime(2021, 12, 25, 0, 0)
+
+ """
return datetime.datetime(
date.year,
date.month,
@@ -40,6 +66,16 @@ def date_to_datetime(date: datetime.date) -> datetime.datetime:
def date_and_time_to_datetime(date: datetime.date,
time: datetime.time) -> datetime.datetime:
+ """
+ Given a date and time, merge them and return a datetime.
+
+ >>> import datetime
+ >>> d = datetime.date(2021, 12, 25)
+ >>> t = datetime.time(12, 30, 0, 0)
+ >>> date_and_time_to_datetime(d, t)
+ datetime.datetime(2021, 12, 25, 12, 30)
+
+ """
return datetime.datetime(
date.year,
date.month,
@@ -47,26 +83,53 @@ def date_and_time_to_datetime(date: datetime.date,
time.hour,
time.minute,
time.second,
- time.millisecond
+ time.microsecond,
)
def datetime_to_date_and_time(
dt: datetime.datetime
) -> Tuple[datetime.date, datetime.time]:
+ """Return the component date and time objects of a datetime.
+
+ >>> import datetime
+ >>> dt = datetime.datetime(2021, 12, 25, 12, 30)
+ >>> (d, t) = datetime_to_date_and_time(dt)
+ >>> d
+ datetime.date(2021, 12, 25)
+ >>> t
+ datetime.time(12, 30)
+
+ """
return (dt.date(), dt.timetz())
def datetime_to_date(dt: datetime.datetime) -> datetime.date:
+ """Return the date part of a datetime.
+
+ >>> import datetime
+ >>> dt = datetime.datetime(2021, 12, 25, 12, 30)
+ >>> datetime_to_date(dt)
+ datetime.date(2021, 12, 25)
+
+ """
return datetime_to_date_and_time(dt)[0]
def datetime_to_time(dt: datetime.datetime) -> datetime.time:
+ """Return the time part of a datetime.
+
+ >>> import datetime
+ >>> dt = datetime.datetime(2021, 12, 25, 12, 30)
+ >>> datetime_to_time(dt)
+ datetime.time(12, 30)
+
+ """
return datetime_to_date_and_time(dt)[1]
-# An enum to represent units with which we can compute deltas.
class TimeUnit(enum.Enum):
+ """An enum to represent units with which we can compute deltas."""
MONDAYS = 0
TUESDAYS = 1
WEDNESDAYS = 2
@@ -101,6 +164,47 @@ def n_timeunits_from_base(
unit: TimeUnit,
base: datetime.datetime
) -> datetime.datetime:
+ """Return a datetime that is N units before/after a base datetime.
+ e.g. 3 Wednesdays from base datetime, 2 weeks from base date, 10
+ years before base datetime, 13 minutes after base datetime, etc...
+ Note: to indicate before/after the base date, use a positive or
+ negative count.
+
+ >>> base = string_to_datetime("2021/09/10 11:24:51AM-0700")[0]
+
+ The next (1) Monday from the base datetime:
+ >>> n_timeunits_from_base(+1, TimeUnit.MONDAYS, base)
+ datetime.datetime(2021, 9, 13, 11, 24, 51, tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=61200)))
+
+ Ten (10) years after the base datetime:
+ >>> n_timeunits_from_base(10, TimeUnit.YEARS, base)
+ datetime.datetime(2031, 9, 10, 11, 24, 51, tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=61200)))
+
+ Fifty (50) working days (M..F, not counting holidays) after base datetime:
+ >>> n_timeunits_from_base(50, TimeUnit.WORKDAYS, base)
+ datetime.datetime(2021, 11, 23, 11, 24, 51, tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=61200)))
+
+ Fifty (50) days (including weekends and holidays) after base datetime:
+ >>> n_timeunits_from_base(50, TimeUnit.DAYS, base)
+ datetime.datetime(2021, 10, 30, 11, 24, 51, tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=61200)))
+
+ Fifty (50) months before (note negative count) base datetime:
+ >>> n_timeunits_from_base(-50, TimeUnit.MONTHS, base)
+ datetime.datetime(2017, 7, 10, 11, 24, 51, tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=61200)))
+
+ Fifty (50) hours after base datetime:
+ >>> n_timeunits_from_base(50, TimeUnit.HOURS, base)
+ datetime.datetime(2021, 9, 12, 13, 24, 51, tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=61200)))
+
+ Fifty (50) minutes before base datetime:
+ >>> n_timeunits_from_base(-50, TimeUnit.MINUTES, base)
+ datetime.datetime(2021, 9, 10, 10, 34, 51, tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=61200)))
+
+ Fifty (50) seconds from base datetime:
+ >>> n_timeunits_from_base(50, TimeUnit.SECONDS, base)
+ datetime.datetime(2021, 9, 10, 11, 25, 41, tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=61200)))
+
+ """
assert TimeUnit.is_valid(unit)
if count == 0:
return base
@@ -110,6 +214,21 @@ def n_timeunits_from_base(
timedelta = datetime.timedelta(days=count)
return base + timedelta
+ # N hours from base
+ elif unit == TimeUnit.HOURS:
+ timedelta = datetime.timedelta(hours=count)
+ return base + timedelta
+
+ # N minutes from base
+ elif unit == TimeUnit.MINUTES:
+ timedelta = datetime.timedelta(minutes=count)
+ return base + timedelta
+
+ # N seconds from base
+ elif unit == TimeUnit.SECONDS:
+ timedelta = datetime.timedelta(seconds=count)
+ return base + timedelta
+
# N workdays from base
elif unit == TimeUnit.WORKDAYS:
if count < 0:
@@ -155,6 +274,7 @@ def n_timeunits_from_base(
base.minute,
base.second,
base.microsecond,
+ base.tzinfo,
)
# N years from base
@@ -168,8 +288,18 @@ def n_timeunits_from_base(
base.minute,
base.second,
base.microsecond,
+ base.tzinfo,
)
+ if unit not in set([TimeUnit.MONDAYS,
+ TimeUnit.TUESDAYS,
+ TimeUnit.WEDNESDAYS,
+ TimeUnit.THURSDAYS,
+ TimeUnit.FRIDAYS,
+ TimeUnit.SATURDAYS,
+ TimeUnit.SUNDAYS]):
+ raise ValueError(unit)
+
# N weekdays from base (e.g. 4 wednesdays from today)
direction = 1 if count > 0 else -1
count = abs(count)
@@ -177,7 +307,7 @@ def n_timeunits_from_base(
start = base
while True:
dow = base.weekday()
- if dow == unit and start != base:
+ if dow == unit.value and start != base:
count -= 1
if count == 0:
return base
@@ -194,14 +324,31 @@ def get_format_string(
include_fractional=False,
twelve_hour=True,
) -> str:
+ """
+ Helper to return a format string without looking up the documentation
+ for strftime.
+
+ >>> get_format_string()
+ '%Y/%m/%d %I:%M:%S%p%z'
+
+ >>> get_format_string(date_time_separator='@')
+ '%Y/%m/%d@%I:%M:%S%p%z'
+
+ >>> get_format_string(include_dayname=True)
+ '%a/%Y/%m/%d %I:%M:%S%p%z'
+
+ >>> get_format_string(include_dayname=True, twelve_hour=False)
+ '%a/%Y/%m/%d %H:%M:%S%z'
+
+ """
fstring = ""
if include_dayname:
fstring += "%a/"
if use_month_abbrevs:
- fstring = f"%Y/%b/%d{date_time_separator}"
+ fstring = f"{fstring}%Y/%b/%d{date_time_separator}"
else:
- fstring = f"%Y/%m/%d{date_time_separator}"
+ fstring = f"{fstring}%Y/%m/%d{date_time_separator}"
if twelve_hour:
fstring += "%I:%M"
if include_seconds:
@@ -229,7 +376,21 @@ def datetime_to_string(
include_fractional=False,
twelve_hour=True,
) -> str:
- """A nice way to convert a datetime into a string."""
+ """
+ A nice way to convert a datetime into a string; arguably better than
+ just printing it and relying on it __repr__().
+
+ >>> d = string_to_datetime(
+ ... "2021/09/10 11:24:51AM-0700",
+ ... )[0]
+ >>> d
+ datetime.datetime(2021, 9, 10, 11, 24, 51, tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=61200)))
+ >>> datetime_to_string(d)
+ '2021/09/10 11:24:51AM-0700'
+ >>> datetime_to_string(d, include_dayname=True, include_seconds=False)
+ 'Fri/2021/09/10 11:24AM-0700'
+
+ """
fstring = get_format_string(
date_time_separator=date_time_separator,
include_timezone=include_timezone,
@@ -251,8 +412,16 @@ def string_to_datetime(
include_fractional=False,
twelve_hour=True,
) -> Tuple[datetime.datetime, str]:
- """A nice way to convert a string into a datetime. Also consider
- dateparse.dateparse_utils for a full parser.
+ """A nice way to convert a string into a datetime. Returns both the
+ datetime and the format string used to parse it. Also consider
+ dateparse.dateparse_utils for a full parser alternative.
+
+ >>> d = string_to_datetime(
+ ... "2021/09/10 11:24:51AM-0700",
+ ... )
+ >>> d
+ (datetime.datetime(2021, 9, 10, 11, 24, 51, tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=61200))), '%Y/%m/%d %I:%M:%S%p%z')
+
"""
fstring = get_format_string(
date_time_separator=date_time_separator,
@@ -268,7 +437,7 @@ def string_to_datetime(
def timestamp() -> str:
- """Return a timestamp for now in Pacific timezone."""
+ """Return a timestamp for right now in Pacific timezone."""
ts = datetime.datetime.now(tz=pytz.timezone("US/Pacific"))
return datetime_to_string(ts, include_timezone=True)
@@ -281,7 +450,25 @@ def time_to_string(
include_timezone=False,
twelve_hour=True,
) -> str:
- """A nice way to convert a datetime into a time (only) string."""
+ """A nice way to convert a datetime into a time (only) string.
+ This ignores the date part of the datetime.
+
+ >>> d = string_to_datetime(
+ ... "2021/09/10 11:24:51AM-0700",
+ ... )[0]
+ >>> d
+ datetime.datetime(2021, 9, 10, 11, 24, 51, tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=61200)))
+
+ >>> time_to_string(d)
+ '11:24:51AM'
+
+ >>> time_to_string(d, include_seconds=False)
+ '11:24AM'
+
+ >>> time_to_string(d, include_seconds=False, include_timezone=True)
+ '11:24AM-0700'
+
+ """
fstring = ""
if twelve_hour:
fstring += "%l:%M"
@@ -308,18 +495,62 @@ MinuteOfDay = NewType("MinuteOfDay", int)
def minute_number(hour: int, minute: int) -> MinuteOfDay:
- """Convert hour:minute into minute number from start of day."""
+ """
+ Convert hour:minute into minute number from start of day.
+
+ >>> minute_number(0, 0)
+ 0
+
+ >>> minute_number(9, 15)
+ 555
+
+ >>> minute_number(23, 59)
+ 1439
+
+ """
return MinuteOfDay(hour * 60 + minute)
def datetime_to_minute_number(dt: datetime.datetime) -> MinuteOfDay:
- """Convert a datetime into a minute number (of the day)"""
+ """
+ Convert a datetime into a minute number (of the day). Note that
+ this ignores the date part of the datetime and only uses the time
+ part.
+
+ >>> d = string_to_datetime(
+ ... "2021/09/10 11:24:51AM-0700",
+ ... )[0]
+
+ >>> datetime_to_minute_number(d)
+ 684
+
+ """
return minute_number(dt.hour, dt.minute)
+def time_to_minute_number(t: datetime.time) -> MinuteOfDay:
+ """
+ Convert a datetime.time into a minute number.
+
+ >>> t = datetime.time(5, 15)
+ >>> time_to_minute_number(t)
+ 315
+
+ """
+ return minute_number(t.hour, t.minute)
+
+
def minute_number_to_time_string(minute_num: MinuteOfDay) -> str:
- """Convert minute number from start of day into hour:minute am/pm
+ """
+ Convert minute number from start of day into hour:minute am/pm
string.
+
+ >>> minute_number_to_time_string(315)
+ ' 5:15a'
+
+ >>> minute_number_to_time_string(684)
+ '11:24a'
+
"""
hour = minute_num // 60
minute = minute_num % 60
@@ -335,7 +566,22 @@ def minute_number_to_time_string(minute_num: MinuteOfDay) -> str:
def parse_duration(duration: str) -> int:
- """Parse a duration in string form."""
+ """
+ Parse a duration in string form into a delta seconds.
+
+ >>> parse_duration('15 days, 2 hours')
+ 1303200
+
+ >>> parse_duration('15d 2h')
+ 1303200
+
+ >>> parse_duration('100s')
+ 100
+
+ >>> parse_duration('3min 2sec')
+ 182
+
+ """
if duration.isdigit():
return int(duration)
seconds = 0
@@ -354,9 +600,24 @@ def parse_duration(duration: str) -> int:
return seconds
-def describe_duration(age: int) -> str:
- """Describe a duration."""
- days = divmod(age, constants.SECONDS_PER_DAY)
+def describe_duration(seconds: int, *, include_seconds = False) -> str:
+ """
+ Describe a duration represented as a count of seconds nicely.
+
+ >>> describe_duration(182)
+ '3 minutes'
+
+ >>> describe_duration(182, include_seconds=True)
+ '3 minutes, and 2 seconds'
+
+ >>> describe_duration(100, include_seconds=True)
+ '1 minute, and 40 seconds'
+
+ describe_duration(1303200)
+ '15 days, 2 hours'
+
+ """
+ days = divmod(seconds, constants.SECONDS_PER_DAY)
hours = divmod(days[1], constants.SECONDS_PER_HOUR)
minutes = divmod(hours[1], constants.SECONDS_PER_MINUTE)
@@ -365,30 +626,65 @@ def describe_duration(age: int) -> str:
descr = f"{int(days[0])} days, "
elif days[0] == 1:
descr = "1 day, "
+
if hours[0] > 1:
descr = descr + f"{int(hours[0])} hours, "
elif hours[0] == 1:
descr = descr + "1 hour, "
- if len(descr) > 0:
+
+ if not include_seconds and len(descr) > 0:
descr = descr + "and "
+
if minutes[0] == 1:
descr = descr + "1 minute"
else:
descr = descr + f"{int(minutes[0])} minutes"
+
+ if include_seconds:
+ descr = descr + ', '
+ if len(descr) > 0:
+ descr = descr + 'and '
+ s = minutes[1]
+ if s == 1:
+ descr = descr + '1 second'
+ else:
+ descr = descr + f'{s} seconds'
return descr
-def describe_duration_briefly(age: int) -> str:
- """Describe a duration briefly."""
- days = divmod(age, constants.SECONDS_PER_DAY)
+def describe_duration_briefly(seconds: int, *, include_seconds=False) -> str:
+ """
+ Describe a duration briefly.
+
+ >>> describe_duration_briefly(182)
+ '3m'
+
+ >>> describe_duration_briefly(182, include_seconds=True)
+ '3m 2s'
+
+ >>> describe_duration_briefly(100, include_seconds=True)
+ '1m 40s'
+
+ describe_duration_briefly(1303200)
+ '15d 2h'
+
+ """
+ days = divmod(seconds, constants.SECONDS_PER_DAY)
hours = divmod(days[1], constants.SECONDS_PER_HOUR)
minutes = divmod(hours[1], constants.SECONDS_PER_MINUTE)
- descr = ""
+ descr = ''
if days[0] > 0:
- descr = f"{int(days[0])}d "
+ descr = f'{int(days[0])}d '
if hours[0] > 0:
- descr = descr + f"{int(hours[0])}h "
- if minutes[0] > 0 or len(descr) == 0:
- descr = descr + f"{int(minutes[0])}m"
+ descr = descr + f'{int(hours[0])}h '
+ if minutes[0] > 0 or (len(descr) == 0 and not include_seconds):
+ descr = descr + f'{int(minutes[0])}m '
+ if minutes[1] > 0 and include_seconds:
+ descr = descr + f'{int(minutes[1])}s'
return descr.strip()
+
+
+if __name__ == '__main__':
+ import doctest
+ doctest.testmod()
diff --git a/executors.py b/executors.py
index e074c30..b243edd 100644
--- a/executors.py
+++ b/executors.py
@@ -64,7 +64,7 @@ SSH = 'ssh -oForwardX11=no'
def make_cloud_pickle(fun, *args, **kwargs):
- logger.info(f"Making cloudpickled bundle at {fun.__name__}")
+ logger.debug(f"Making cloudpickled bundle at {fun.__name__}")
return cloudpickle.dumps((fun, args, kwargs))
diff --git a/text_utils.py b/text_utils.py
index 8ea6e19..49ff9b9 100644
--- a/text_utils.py
+++ b/text_utils.py
@@ -268,6 +268,28 @@ class Indenter:
print(self.pad_prefix + self.padding * self.level + text, end='')
+def header(title: str, *, width: int = 80, color: str = ''):
+ """
+ Returns a nice header line with a title.
+
+ >>> header('title', width=60, color='')
+ '----[ title ]-----------------------------------------------'
+
+ """
+ w = width
+ w -= (len(title) + 4)
+ if w >= 4:
+ left = 4 * '-'
+ right = (w - 4) * '-'
+ if color != '' and color is not None:
+ r = reset()
+ else:
+ r = ''
+ return f'{left}[ {color}{title}{r} ]{right}'
+ else:
+ return ''
+
+
if __name__ == '__main__':
import doctest
doctest.testmod()
diff --git a/type/centcount.py b/type/centcount.py
index ce18975..4e5b8a6 100644
--- a/type/centcount.py
+++ b/type/centcount.py
@@ -11,7 +11,8 @@ T = TypeVar('T', bound='CentCount')
class CentCount(object):
"""A class for representing monetary amounts potentially with
- different currencies.
+ different currencies meant to avoid floating point rounding
+ issues by treating amount as a simple integral count of cents.
"""
def __init__ (
diff --git a/unittest_utils.py b/unittest_utils.py
index 2dc8cfe..8a0556b 100644
--- a/unittest_utils.py
+++ b/unittest_utils.py
@@ -148,15 +148,19 @@ def check_all_methods_for_perf_regressions(prefix='test_'):
def breakpoint():
+ """Hard code a breakpoint somewhere; drop into pdb."""
import pdb
pdb.set_trace()
class RecordStdout(object):
"""
- with uu.RecordStdout() as record:
- print("This is a test!")
- print({record().readline()})
+ Record what is emitted to stdout.
+
+ >>> with RecordStdout() as record:
+ ... print("This is a test!")
+ >>> print({record().readline()})
+ {'This is a test!\\n'}
"""
def __init__(self) -> None:
@@ -176,9 +180,13 @@ class RecordStdout(object):
class RecordStderr(object):
"""
- with uu.RecordStderr() as record:
- print("This is a test!", file=sys.stderr)
- print({record().readline()})
+ Record what is emitted to stderr.
+
+ >>> import sys
+ >>> with RecordStderr() as record:
+ ... print("This is a test!", file=sys.stderr)
+ >>> print({record().readline()})
+ {'This is a test!\\n'}
"""
def __init__(self) -> None:
@@ -198,12 +206,9 @@ class RecordStderr(object):
class RecordMultipleStreams(object):
"""
- with uu.RecordStreams(sys.stderr, sys.stdout) as record:
- print("This is a test!")
- print("This is one too.", file=sys.stderr)
-
- print(record().readlines())
+ Record the output to more than one stream.
"""
+
def __init__(self, *files) -> None:
self.files = [*files]
self.destination = tempfile.SpooledTemporaryFile(mode='r+')
@@ -219,3 +224,8 @@ class RecordMultipleStreams(object):
for f in self.files:
f.write = self.saved_writes.pop()
self.destination.seek(0)
+
+
+if __name__ == '__main__':
+ import doctest
+ doctest.testmod()