Skip to content

Instantly share code, notes, and snippets.

@yatharthb97
Last active July 16, 2022 15:41
Show Gist options
  • Save yatharthb97/8798b23a1611a90b056e2026bbed63a8 to your computer and use it in GitHub Desktop.
Save yatharthb97/8798b23a1611a90b056e2026bbed63a8 to your computer and use it in GitHub Desktop.
A module for managing and parsing string to time literals in python.
#!/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