from dataclasses import dataclass
from typing import Callable, Generator, Optional
from task_script_utils.parse import to_float
from ts_ids_core.schema import RawValueUnit
from ts_lib_parsy import Parser, generate, regex, seq, success
from ts_lib_units import Unit, UnitParser
from ts_lib_parsy_components.base import scientific_or_float
_optional_open_bracket = regex(r"[\[\(]?").desc("open bracket")
_optional_close_bracket = regex(r"[\]\)]?").desc("close bracket")
_optional_spaces = regex("[ \t]*").desc("spaces and tabs")
_unit = regex(r"[\w/μ°Å%]+").desc("unit")
[docs]
@dataclass
class ValueUnitParser:
"""Parsers for raw value unit strings
Args:
unit_parser: A UnitParser object.
value_fn: A parser for the raw value string.
unit_fn: A parser for the raw unit string.
float_converter: A function to convert the raw value string to a float.
default_unit: The default unit to use if no unit is found.
"""
unit_parser: UnitParser
value_parser: Parser[str] = scientific_or_float
raw_unit_parser: Parser[str] = _unit
float_converter: Callable[[str], Optional[float]] = to_float
default_unit: str = Unit.ARBITRARY_UNIT
[docs]
def value_optional_unit(self) -> Parser[RawValueUnit]:
"""Create a parser for RawValueUnit objects where the unit is optional with a default.
Returns:
A parser for RawValueUnit objects.
"""
return self.value_unit() | self.value()
[docs]
def value_unit(self, raw_unit: Optional[str] = None) -> Parser[RawValueUnit]:
"""Create a parser for RawValueUnit objects, with a required unit.
By default this function assumes that the unit appears after the value in the
stream.
If the unit appears elsewhere in the file, the `raw_unit` kwarg can be provided.
The returned parser will parse the value string from the stream, but the input
`raw_unit` will be used as the raw unit string, instead of parsing it from
the stream.
Keyword Arguments:
`raw_unit` -- A raw unit string to accompany with value (default: {None})
Returns:
A parser for RawValueUnit objects.
"""
@generate
def parser() -> Generator[Parser[str], str, RawValueUnit]:
"""Parse a raw value unit string to a RawValueUnit object."""
if raw_unit is not None:
# If raw_unit is provided, we don't need to parse it from the stream
raw_value_parser = self.value_parser + success(" " + raw_unit)
else:
# If raw_unit is not provided, we parse it from the stream
raw_value_parser = (
self.value_parser
+ _optional_spaces
+ _optional_open_bracket
+ self.raw_unit_parser
+ _optional_close_bracket
)
raw_value = yield raw_value_parser
extract_unit_parser = (
_optional_spaces
>> _optional_open_bracket
>> self.raw_unit_parser
<< _optional_close_bracket
)
value, unit = seq(self.value_parser, extract_unit_parser).parse(raw_value)
return RawValueUnit(
raw_value=raw_value,
value=self.float_converter(value),
unit=self.unit_parser.parse(unit),
)
return parser
[docs]
def value(self, unit: Optional[Unit] = None) -> Parser[RawValueUnit]:
"""Create a parser for RawValueUnit objects, with a default unit.
By default the unit is taken from the `default_unit` attribute.
If you wish to override this default, you can provide a `unit` kwarg.
Keyword Arguments:
unit -- The unit to store alongside the value (default: {None})
Returns:
A parser for RawValueUnit objects.
"""
@generate
def parser() -> Generator[Parser[str], str, RawValueUnit]:
"""Parse a raw value string to a RawValueUnit object."""
value = yield self.value_parser
return RawValueUnit(
raw_value=value,
value=self.float_converter(value),
unit=unit or self.default_unit,
)
return parser