Last active
July 16, 2022 15:41
-
-
Save yatharthb97/8798b23a1611a90b056e2026bbed63a8 to your computer and use it in GitHub Desktop.
A module for managing and parsing string to time literals in python.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
#!/usr/bin/env python | |
''' | |
Time Literals (tmliteral.py) | |
--------------------------- | |
Author: Yatharth Bhasin (GitHub → yatharthb97) | |
Brief: A module for managing and parsing string to time literals in python. | |
This piece of software was released on GistHub → https://gist.github.com/yatharthb97/8798b23a1611a90b056e2026bbed63a8 | |
Web Article / Explainer : https://yatharthb97.github.io/projects/python-time-literals/ | |
Licence: *MIT open-source license* https://opensource.org/licenses/mit-license.php) | |
Please give credits wherever you can. (https://www.wikihow.com/Cite-a-GitHub-Repository) | |
→ Use as submodule: `git submodule add <repo-link> --name tmliteral` . | |
→ Use `git mv <oldname> <newname>` to properly change the repo folder name to something relevant | |
if an arbitrary name is given. | |
Module: tmliteral - tmliteral.py - Time Literals | |
Description: | |
------------ | |
This module can be used to define, convert, and parse time literals. Hence, user can input strings like | |
"4.2ns", "5e6 ms", ".32e5 microseconds", which are all valid inputs. The `TmParser` object in the module | |
can be used to parse these strings into a value-pair, which consists of a flot value and a `TmLiteral` object. | |
The `TmLiteral` object is aware of the time units w.r.t. the SI base in seconds. And hence can be used for | |
inter-conversions and formatted printing. | |
0. Intrinsics: | |
* value pair: A pair (any container that can be accessed by `[]` or the `__get_item__` method) that has its first | |
value as a float and second value as a `TmLiteral` object. | |
* `time_table`: A tuple that contains all the known pre-defined time literals. It is automatically added to | |
`TmParser.literals` list for parsing. | |
* A string without a literal, eg "4.2" defaults to seconds. Hence, "4.2" isequivalent to "4.2 seconds" | |
* Spaces in strings are automatically removed before parsing. | |
1. Object List: | |
* `TmLiteral` (alias `TmL`): Defines a time literal. | |
* `TmParser`: Parser object that converts a string to a value pair. | |
Eg: | |
parser = TmParser() | |
tm1 = parser.parse("4.2fs") # Out→ [4.2, < TmLiteral : femtosecond >] | |
tm2 = parser["5 ms"] # Same as `parse`. | |
* `TmCast`: A casting object that can convert any arbitrary value-pair to a pre-fixed unit. | |
Eg: | |
ns_cast = TmCast.Find("ns") #Make a nano-second cast | |
tm2_ns = ns_cast.cast(tm2) | |
tm2_ns = ns_cast[tm2] # Equivalent to `cast` member function. | |
* `TmExpParser` : Parse a heterogenous expression with multiple units. | |
Eg: | |
exp = TmExpParser() | |
print(exp.parse("10ms + 5ns + 10ms")) -> prints [0.020000005, < TmLiteral : second >] | |
* `TmAutoScale` : Finds the most suitable units and performs the conversion. | |
Eg: | |
tmu = TmAutoScale(mode="prefer_clock_units") | |
tauto = tmu.scale(parser["4e5 picoseconds"], set_scale=100) | |
print(tauto) -> prints [399.99999999999994, < TmLiteral : nanosecond >] | |
2. Helper Functions: | |
* FindTmL : Finds the appropriate `TmLiteral` object for the given string. (Valid keys are | |
abbrevaition, singular, and plural forms.) | |
* IsNumeric : Returns `True` if the given sting is a valid `float` upon conversion. | |
* IsValuePair : Returns `True` is the given container is a valid value pair -> [float, TmLiteral]. | |
* TmFormat : Returns a formatted string for a given valid value pair. | |
3. TODO | |
* Fix Rounding | |
-- | |
→ Project timeline: | |
+ Start → 13-Sept-2021 (After Lunch) | |
+ Finished → 10-May-2022 (After Lunch) | |
''' | |
# Import statements. | |
import re | |
import time | |
from copy import deepcopy | |
import math | |
from collections import OrderedDict | |
#1 | |
class TmLiteral: | |
'''TmLiteral is used to define a literal object that encapsulates | |
functions for inter-converion, display, etc.''' | |
def __init__(self, abbr, exp, name, mul_factor=1.0): | |
'''Constructor that takes the following arguemnts: | |
abrr: Abbreviation of the literal | |
exp: Multiplicative exponent -> power of 10 | |
string: Complete string representation | |
mul_factor: Multiplicative factor (optional specification).''' | |
self.abbr = abbr | |
self.exp = exp | |
self.name = name | |
self.mul_factor = mul_factor | |
self.group = (self.abbr, self.name, self.name + 's') | |
def match(self, string): | |
'''Alias of `__contains__`.''' | |
return __contains__(string) | |
def plural(self): | |
'''Returns the plural from of `self.name`.''' | |
return self.name + 's' | |
def __contains__(self, string): | |
'''Returns `True` if the given string matches the literal.''' | |
return string in self.group | |
def __str__(self): | |
return self.name | |
def __repr__(self): | |
return f"< TmLiteral : {self.name} >" | |
def multiplier(self): | |
'''`SI multiplier.` | |
Returns a multiplier that converts the literal to the SI base, i.e. seconds.''' | |
return float(10**self.exp) * self.mul_factor | |
def in_seconds(value): | |
'''Returns any given value in seconds.''' | |
return value*self.multiplier() | |
def is_valid(self): | |
'''Returns `True` if the literal object is valid for conversions.''' | |
return not(math.isnan(self.exp) and math.isnan(self.mul_factor)) | |
#1.1 | |
class TmL(TmLiteral): | |
'''`TmL` is defined as an alias of `TmLiteral` class.''' | |
pass | |
# Fixed TimeTable tuple of known (default) time literals and support functions. | |
#2 | |
'''Tuple of predefined literals.''' | |
time_table = ( TmL('xxx', math.nan, "invalid time value", mul_factor=math.nan), | |
TmL('s', 0, "second"), | |
TmL('Ys', 24, "yottasecond"), | |
TmL('Zs', 21, "zettasecond"), | |
TmL('Es', 18, "exasecond"), | |
TmL('Ps', 15, "petasecond"), | |
TmL('Ts', 12, "terasecond"), | |
TmL('Gs', 9, "gigasecond"), | |
TmL('Ms', 6, "megasecond"), | |
TmL('ks', 3, "kilosecond"), | |
TmL('hs', 2, "hectosecond"), | |
TmL('das', 1, "decasecond"), | |
TmL('ds', -1, "decisecond"), | |
TmL('cs', -2, "centisecond"), | |
TmL('ms', -3, "millisecond"), | |
TmL('us', -6, "microsecond"), | |
TmL('ns', -9, "nanosecond"), | |
TmL('ps', -12, "picosecond"), | |
TmL('fs', -15, "femtosecond"), | |
TmL('as', -18, "attosecond"), | |
TmL('zs', -21, "zeptosecond"), | |
TmL('ys', -24, "yoctosecond"), | |
TmL('min', 1, "minute", mul_factor=6), | |
TmL('h', 3, "hour", mul_factor=3.600), | |
TmL('d', 4, "day", mul_factor=8.6400), | |
TmL('week', 5, "week", mul_factor=6.04800), | |
TmL('year', 7, "calender year", mul_factor=3.1536000), | |
TmL('leapyear', 7, "calender leap year", mul_factor=3.1622400), | |
TmL('gregyear', 7, "gregorian calendar year", mul_factor=3.1556952), | |
TmL('julianyear', 7, "julian astronomical year", mul_factor=3.1557600)) | |
#3 | |
def FindTmL(string): | |
'''Function that finds the appropriate TmLiteral object from | |
tuple `time_table` and returns a deepcopy of it.''' | |
tml = deepcopy(time_table[0]) | |
for obj in time_table: | |
if string in obj: | |
tml = deepcopy(obj) | |
return tml | |
#4 | |
def IsNumeric(string): | |
'''Returns `True` if the given string is a valid float constant upon conversion.''' | |
try: | |
float(string) | |
return True | |
except ValueError: | |
return False | |
#5 | |
def IsValuePair(pair): | |
'''Returns true if the given container is a valid `value pair`. | |
A valid `value pair` has its first member a valid float value | |
and the second memeber an instance of `TmLiteral` class.''' | |
if not isinstance(pair, list): | |
return False | |
return isinstance(pair[0], float) and isinstance(pair[1], TmLiteral) | |
#6 | |
def TmFormat(pair, format="full", prec=3): | |
'''For a given valid value pair, it prints a correct string representation.''' | |
if format is any(["abbr", "full"]): | |
raise KeyError("Invalid parameter value for `format` {'abbr', 'full'}.") | |
if format == "abbr": | |
return f"{pair[0]:.{prec}f} {pair[1].abbr}" | |
else: | |
return f"{pair[0]:.{prec}f} {(abs(pair[0]) > 1.0) * (pair[1].plural()) + (abs(pair[0]) <= 1.0) * (pair[1].__str__())}" | |
#7 | |
class TmCast: | |
'''TmCast is an object that can be used to convert any literal value | |
to a predefined unit(literal) set during object construction.''' | |
def __init__(self, tm_literal): | |
'''Constructor that creates a cast around the given TmLiteral object. | |
tm_literal : Must be a `TmLiteral` instance, else TypeError is thrown.''' | |
if not isinstance(tm_literal, TmLiteral): | |
raise TypeError("Passed object must be an instance of `TmLiteral`.") | |
self.base = tm_literal | |
def Find(key): | |
'''Factory function that finds the appropriate TmLiteral object from | |
tuple `time_table` and returns its TmCast object.''' | |
for obj in time_table: | |
if key in obj: | |
return TmCast(deepcopy(obj)) | |
return None | |
def cast(self, val_pair): | |
'''Returns a single numeric value that is casted to the base type of the `TmCast` object. | |
val_pair: Any container having the following form - [<numeric_value>, TmLiteral].''' | |
value = val_pair[0] | |
value_literal = val_pair[1] | |
# If a valid string literal is passed, it is converted to the appropriate TmLiteral. | |
if isinstance(value_literal, str): | |
value_literal = TmParser(value_literal) | |
return float(value) * float(value_literal.multiplier()) / self.base.multiplier() | |
def cast_pair(self, val_pair): | |
'''Returns a pair-tuple of the casted numeric value and the base `TmLiteral` object. | |
val_pair: Any container having the following form - [<numeric_value>, TmLiteral].''' | |
value = val_pair[0] | |
value_literal = val_pair[1] | |
from copy import deepcopy | |
return (self.cast(val_pair), deepcopy(self.base)) | |
def __repr__(self): | |
return f"< TmCast to `{self.base}` >" | |
def __str__(self): | |
return f"TmCast object encapsulating `{self.base.__str__()}`" | |
def __getitem__(self, pair): | |
'''`[value_pair]` operator overload for class that works the same as `cast` member function.''' | |
return self.cast(pair) | |
#8 | |
class TmParser: | |
'''TmParser is an object that converts a given string into a `value_pair`.''' | |
def __init__(self, more_literals=None): | |
'''Create a `TmParser` object with the `time_table` tuple of recognized `TmLiteral` objects. | |
more_literals : Add more user-defined `TmLiterals` to the parser environment. ''' | |
# List of parsable literals. | |
self.literals = list(time_table) | |
if not more_literals == None: | |
self.literals.extend(list(more_literals)) | |
def parse(self, time_literal_str): | |
'''Parses the given string into a value pair. | |
Returns an TmL("invalid time") instance if the first member is | |
not a valid float string, or the parser is unable to find a valid | |
TmLiteral for the given string.''' | |
# Clean | |
clean = time_literal_str.replace(' ', '') # Remove spaces | |
#Tokenize | |
proto_val_pair = re.findall("\\D+|.*[+-]?[0-9]*[.]?[0-9]+(?:[eE][-+]?[0-9]+)?", clean) | |
# Default to seconds if no literal is given | |
if len(proto_val_pair) == 1: | |
proto_val_pair.append("seconds") | |
# If parse was not successful. | |
if len(proto_val_pair) == 0: | |
return None | |
#Parse → If atlease two values are present. | |
if not IsNumeric(proto_val_pair[0]): | |
return [0, deepcopy(time_table[0])] #Return invalid time | |
else: | |
return [float(proto_val_pair[0]), FindTmL(proto_val_pair[1])] | |
def __getitem__(self, string): | |
'''`["4.2 ns"]` operator overload for class that works the same as `parse` member function.''' | |
return self.parse(string) | |
def add_literals(self, other_literals): | |
'''Add more user-defined literals to the Parser object.''' | |
self.literals.extend(list(other_literals)) | |
# 9 | |
class TmAutoScale: | |
'''Casts the literal object to the mot appropriate unit. | |
Ideal form is a float with format x.yyy units.''' | |
# Seconds Cast | |
sec_cast = TmCast.Find("s") | |
def __init__(self, tm_parser=None, mode="default"): | |
'''`TmAutoScale` object constructor. | |
tm_parser: [optional] Parser object from which the list of literals is | |
inherited. If not passed, a new `TmParser` object is constructed. | |
["calender leap year","gregorian calendar year", "julian astronomical year"] | |
are not considered by the auto-scaler. | |
mode = {"default", prefer_clock_units", "prefer_SI_units", clock", "SI", "extended_process_timing"} | |
clock units are: ["second", "minute", "hour", "day", "week", "calender year"]''' | |
mode = mode.lower() | |
# Populate Parser Object | |
if isinstance(tm_parser, TmParser): | |
self.tmparser = deepcopy(tm_parser) | |
else: # Construct new | |
self.tmparser = TmParser() | |
literals = deepcopy(self.tmparser.literals) | |
# Filtering | |
literals = [tml for tml in self.tmparser.literals \ | |
if tml.abbr not in ['xxx', 'leapyear', 'gregyear', 'julianyear']] | |
# Filter Table | |
if mode != "default": | |
clock = [] | |
si = [] | |
si.append(FindTmL("s")) # Duplicate seconds cast | |
for tml in literals: | |
if tml.name in ["second", "minute", "hour", "day", "week", "calender year"]: | |
clock.append(tml) | |
else: | |
si.append(tml) | |
if mode == "prefer_clock_units": | |
literals = clock + si | |
elif mode == "prefer_clock_units": | |
literals = si + clock | |
elif mode == "si": | |
literals = si | |
elif mode == "clock": | |
literals = clock | |
if mode == "extended_process_timing": | |
literals = [FindTmL("ns"), FindTmL("us"), FindTmL("ms")] + clock | |
print(literals) | |
# Build Exp table | |
self.exptable = OrderedDict() | |
for entity in literals: | |
if not entity.exp in self.exptable: | |
self.exptable[entity.exp] = entity | |
def scale(self, val_pair, set_scale=1): | |
'''Scale the given value pair to approprite units. | |
val_pair: Value pair to scale. If a string is passed, it is parsed. | |
set_scale: Shifts output scale from 10 to `set_scale`.''' | |
def frexp(number): | |
'''frexp for decimal base.''' | |
from decimal import Decimal | |
(sign, digits, exponent) = Decimal(number).as_tuple() | |
return len(digits) + exponent - 1 | |
# Parse string | |
if isinstance(val_pair, str): | |
val_pair = self.tmparser[val_pair] | |
# Convert to seconds | |
scale_exp = frexp(set_scale) | |
sval = self.sec_cast[val_pair] | |
value_exp = frexp(sval) | |
# Match lowest & consider only positive scale | |
exp_list = list(self.exptable.keys()) | |
diff = [abs(exp - value_exp + scale_exp) for exp in exp_list] | |
best_index = diff.index(min(diff)) | |
suitable_exp = self.exptable[exp_list[best_index]] | |
caster = TmCast(suitable_exp) | |
casted = caster[[sval, self.sec_cast.base]] | |
return [casted, caster.base] | |
def __getitem__(self, val_pair): | |
''' Alias of `scale` but does not have `set_scale` feature.''' | |
return self.scale(val_pair) | |
# 10 | |
class TmExpParser: | |
'''`TmExpParser` is an object that parses an expression with heterogenous time literals.''' | |
# Seconds Parser | |
sec_cast = TmCast.Find("s") | |
def __init__(self): | |
''' Constructor. ''' | |
pass | |
def parse(self, string, tm_cast=None): | |
'''Parse a string expression - perform arithmetic operation with | |
different / heterogenous time literals. | |
Raises a Value error if there is an invalid character(s). | |
Valid operations -> {+, -, *, /, %}. | |
string : Expression as a string - "4.23ns + 3.6ms - 4.2s" | |
tm_cast : Base unit for the return value (must be a `TmLiteral` type). | |
If value is not passed, the function returns a value pair in seconds.''' | |
if tm_cast == None: | |
tm_cast = self.sec_cast | |
# Clean | |
string = string.replace(' ', '') | |
# Tokenize | |
import re | |
tokens = re.split("(\+|\-|\*|\%|/)", string) | |
# Create Pairs | |
for i, tok in enumerate(tokens): | |
if tok not in '+-*/%': | |
tm = parser[tok] | |
if tm[1].is_valid(): | |
tokens[i] = tm | |
else: | |
raise ValueError(f"Invalid string literal parsed: {tok}") | |
# Convert to same base | |
for i, tok in enumerate(tokens): | |
if IsValuePair(tok): | |
tokens[i] = tm_cast[tok] | |
def gen_var_name(): | |
i = 0 | |
while True: | |
yield 'var_' + str(i) | |
i += 1 | |
dict_ = {} | |
gen = gen_var_name() | |
for idx, item in enumerate(tokens): | |
if isinstance(item, float): | |
var_name = next(gen) | |
dict_.update({var_name : item}) | |
tokens[idx] = var_name | |
str_tokens = ' '.join(tokens) | |
result = eval(str_tokens, None, dict_) | |
return [result, deepcopy(tm_cast.base)] | |
def __getitem__(self, string): | |
'''Alias of parse. Usage : `expression = exp_parser["5.2ns + 6.3ns"]`.''' | |
return self.parse(string) | |
def __getitem__(self, string, tm_cast=None): | |
'''Alias of parse. Usage : `expression = exp_parser["5.2ns + 6.3ns"]`.''' | |
return self.parse(string, tm_cast=tm_cast) | |
#--- | |
#11 | |
# Tests | |
''' | |
if __name__ == "__main__": | |
cst = TmCast.Find("ns") | |
casted = cst[[1.5, TmL("ms", -6, "microsecond")]] | |
print(casted) | |
casted = cst.cast_pair([1.5, TmL("ms", -6, "microsecond")]) | |
print(casted) | |
parser = TmParser() | |
print(TmFormat(parser["1.01 ns"])) | |
print(parser["4.2 ms"]) | |
print(parser["4.2 days"]) | |
print(parser["4.2 femtosecond"]) | |
print(parser["4e5 picoseconds"]) | |
print(parser["4e5"]) | |
print(parser[""]) | |
print(IsValuePair(parser["4e5 picoseconds"])) | |
print(TmFormat(parser["4.2x"])) | |
ms_cast = TmCast.Find("ms") | |
exp = TmExpParser() | |
print(exp.parse("10ms + 5ns + 10ms")) | |
tmu = TmAutoScale(mode ="prefer_clock_units") | |
tauto = tmu.scale(parser["4e5 picoseconds"], set_scale=100) | |
print(tauto) | |
tmu2 = TmAutoScale(mode ="clock") | |
print('\n\n') | |
tmu3 = TmAutoScale(mode ="SI") | |
print('\n\n') | |
tmu3 = TmAutoScale(mode ="extended_process_timing") | |
''' |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment