implemented _parse_date function to formalise date parsing
made Series a subclass of UserList to improve compatibility
This commit is contained in:
parent
053a93900a
commit
cbace875c1
131
fincal/core.py
131
fincal/core.py
@ -1,4 +1,5 @@
|
||||
import datetime
|
||||
from collections import UserList
|
||||
from dataclasses import dataclass
|
||||
from numbers import Number
|
||||
from typing import Iterable, List, Literal, Mapping, Sequence, Tuple, Union
|
||||
@ -6,8 +7,8 @@ from typing import Iterable, List, Literal, Mapping, Sequence, Tuple, Union
|
||||
|
||||
@dataclass
|
||||
class FincalOptions:
|
||||
date_format: str = '%Y-%m-%d'
|
||||
closest: str = 'before' # after
|
||||
date_format: str = "%Y-%m-%d"
|
||||
closest: str = "before" # after
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@ -20,12 +21,12 @@ class Frequency:
|
||||
|
||||
|
||||
class AllFrequencies:
|
||||
D = Frequency('daily', 'days', 1, 1, 'D')
|
||||
W = Frequency('weekly', 'days', 7, 7, 'W')
|
||||
M = Frequency('monthly', 'months', 1, 30, 'M')
|
||||
Q = Frequency('quarterly', 'months', 3, 91, 'Q')
|
||||
H = Frequency('half-yearly', 'months', 6, 182, 'H')
|
||||
Y = Frequency('annual', 'years', 1, 365, 'Y')
|
||||
D = Frequency("daily", "days", 1, 1, "D")
|
||||
W = Frequency("weekly", "days", 7, 7, "W")
|
||||
M = Frequency("monthly", "months", 1, 30, "M")
|
||||
Q = Frequency("quarterly", "months", 3, 91, "Q")
|
||||
H = Frequency("half-yearly", "months", 6, 182, "H")
|
||||
Y = Frequency("annual", "years", 1, 365, "Y")
|
||||
|
||||
|
||||
def _preprocess_timeseries(
|
||||
@ -33,9 +34,9 @@ def _preprocess_timeseries(
|
||||
Sequence[Iterable[Union[str, datetime.datetime, float]]],
|
||||
Sequence[Mapping[str, Union[float, datetime.datetime]]],
|
||||
Sequence[Mapping[Union[str, datetime.datetime], float]],
|
||||
Mapping[Union[str, datetime.datetime], float]
|
||||
Mapping[Union[str, datetime.datetime], float],
|
||||
],
|
||||
date_format: str
|
||||
date_format: str,
|
||||
) -> List[Tuple[datetime.datetime, float]]:
|
||||
"""Converts any type of list to the correct type"""
|
||||
|
||||
@ -75,12 +76,12 @@ def _preprocess_timeseries(
|
||||
def _preprocess_match_options(as_on_match: str, prior_match: str, closest: str) -> datetime.timedelta:
|
||||
"""Checks the arguments and returns appropriate timedelta objects"""
|
||||
|
||||
deltas = {'exact': 0, 'previous': -1, 'next': 1}
|
||||
deltas = {"exact": 0, "previous": -1, "next": 1}
|
||||
if closest not in deltas.keys():
|
||||
raise ValueError(f"Invalid closest argument: {closest}")
|
||||
|
||||
as_on_match = closest if as_on_match == 'closest' else as_on_match
|
||||
prior_match = closest if prior_match == 'closest' else prior_match
|
||||
as_on_match = closest if as_on_match == "closest" else as_on_match
|
||||
prior_match = closest if prior_match == "closest" else prior_match
|
||||
|
||||
if as_on_match in deltas.keys():
|
||||
as_on_delta = datetime.timedelta(days=deltas[as_on_match])
|
||||
@ -95,6 +96,24 @@ def _preprocess_match_options(as_on_match: str, prior_match: str, closest: str)
|
||||
return as_on_delta, prior_delta
|
||||
|
||||
|
||||
def _parse_date(date: str, date_format: str = None):
|
||||
"""Parses date and handles errors"""
|
||||
|
||||
if isinstance(date, (datetime.datetime, datetime.date)):
|
||||
return datetime.datetime.fromordinal(date.toordinal())
|
||||
|
||||
if date_format is None:
|
||||
date_format = FincalOptions.date_format
|
||||
|
||||
try:
|
||||
date = datetime.datetime.strptime(date, date_format)
|
||||
except TypeError:
|
||||
raise Exception("Date does not seem to be valid date-like string")
|
||||
except ValueError:
|
||||
raise Exception("Date could not be parsed. Have you set the correct date format in FincalOptions.date_format?")
|
||||
return date
|
||||
|
||||
|
||||
class _IndexSlicer:
|
||||
def __init__(self, parent_obj):
|
||||
self.parent = parent_obj
|
||||
@ -112,7 +131,7 @@ class _IndexSlicer:
|
||||
return item
|
||||
|
||||
|
||||
class Series:
|
||||
class Series(UserList):
|
||||
def __init__(self, data):
|
||||
if not isinstance(data, Sequence):
|
||||
raise TypeError("Series only supports creation using Sequence types")
|
||||
@ -128,27 +147,26 @@ class Series:
|
||||
data = [datetime.datetime.strptime(i, FincalOptions.date_format) for i in data]
|
||||
self.dtype = datetime.datetime
|
||||
except ValueError:
|
||||
raise TypeError("Series does not support string data type except dates.\n"
|
||||
"Hint: Try setting the date format using FincalOptions.date_format")
|
||||
elif isinstance(data[0], datetime.datetime):
|
||||
raise TypeError(
|
||||
"Series does not support string data type except dates.\n"
|
||||
"Hint: Try setting the date format using FincalOptions.date_format"
|
||||
)
|
||||
elif isinstance(data[0], (datetime.datetime, datetime.date)):
|
||||
self.dtype = datetime.datetime
|
||||
self.data = data
|
||||
self.data = [_parse_date(i) for i in data]
|
||||
else:
|
||||
raise TypeError(f"Cannot create series object from {type(data).__name__} of {type(data[0]).__name__}")
|
||||
|
||||
def __repr__(self):
|
||||
return f"{self.__class__.__name__}({self.data})"
|
||||
|
||||
def __getitem__(self, n):
|
||||
return self.data[n]
|
||||
|
||||
def __len__(self):
|
||||
return len(self.data)
|
||||
|
||||
def __gt__(self, other):
|
||||
if self.dtype == bool:
|
||||
raise TypeError("> not supported for boolean series")
|
||||
|
||||
if isinstance(other, (str, datetime.datetime, datetime.date)):
|
||||
other = _parse_date(other)
|
||||
|
||||
if self.dtype == float and isinstance(other, Number) or isinstance(other, self.dtype):
|
||||
gt = Series([i > other for i in self.data])
|
||||
else:
|
||||
@ -178,10 +196,7 @@ class TimeSeriesCore:
|
||||
"""Defines the core building blocks of a TimeSeries object"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
data: List[Iterable],
|
||||
frequency: Literal['D', 'W', 'M', 'Q', 'H', 'Y'],
|
||||
date_format: str = "%Y-%m-%d"
|
||||
self, data: List[Iterable], frequency: Literal["D", "W", "M", "Q", "H", "Y"], date_format: str = "%Y-%m-%d"
|
||||
):
|
||||
"""Instantiate a TimeSeries object
|
||||
|
||||
@ -241,42 +256,42 @@ class TimeSeriesCore:
|
||||
|
||||
printable = {}
|
||||
iter_f = iter(self.time_series)
|
||||
first_n = [next(iter_f) for i in range(n//2)]
|
||||
first_n = [next(iter_f) for i in range(n // 2)]
|
||||
|
||||
iter_b = reversed(self.time_series)
|
||||
last_n = [next(iter_b) for i in range(n//2)]
|
||||
last_n = [next(iter_b) for i in range(n // 2)]
|
||||
last_n.sort()
|
||||
|
||||
printable['start'] = [str((i, self.time_series[i])) for i in first_n]
|
||||
printable['end'] = [str((i, self.time_series[i])) for i in last_n]
|
||||
printable["start"] = [str((i, self.time_series[i])) for i in first_n]
|
||||
printable["end"] = [str((i, self.time_series[i])) for i in last_n]
|
||||
return printable
|
||||
|
||||
def __repr__(self):
|
||||
if len(self.time_series) > 6:
|
||||
printable = self._get_printable_slice(6)
|
||||
printable_str = "{}([{}\n\t ...\n\t {}], frequency={})".format(
|
||||
self.__class__.__name__,
|
||||
',\n\t '.join(printable['start']),
|
||||
',\n\t '.join(printable['end']),
|
||||
repr(self.frequency.symbol)
|
||||
)
|
||||
self.__class__.__name__,
|
||||
",\n\t ".join(printable["start"]),
|
||||
",\n\t ".join(printable["end"]),
|
||||
repr(self.frequency.symbol),
|
||||
)
|
||||
else:
|
||||
printable_str = "{}([{}], frequency={})".format(
|
||||
self.__class__.__name__,
|
||||
',\n\t'.join([str(i) for i in self.time_series.items()]),
|
||||
repr(self.frequency.symbol)
|
||||
)
|
||||
self.__class__.__name__,
|
||||
",\n\t".join([str(i) for i in self.time_series.items()]),
|
||||
repr(self.frequency.symbol),
|
||||
)
|
||||
return printable_str
|
||||
|
||||
def __str__(self):
|
||||
if len(self.time_series) > 6:
|
||||
printable = self._get_printable_slice(6)
|
||||
printable_str = "[{}\n ...\n {}]".format(
|
||||
',\n '.join(printable['start']),
|
||||
',\n '.join(printable['end']),
|
||||
)
|
||||
",\n ".join(printable["start"]),
|
||||
",\n ".join(printable["end"]),
|
||||
)
|
||||
else:
|
||||
printable_str = "[{}]".format(',\n '.join([str(i) for i in self.time_series.items()]))
|
||||
printable_str = "[{}]".format(",\n ".join([str(i) for i in self.time_series.items()]))
|
||||
return printable_str
|
||||
|
||||
def __getitem__(self, key):
|
||||
@ -288,27 +303,25 @@ class TimeSeriesCore:
|
||||
else:
|
||||
dates_to_return = [self.dates[i] for i, j in enumerate(key) if j]
|
||||
data_to_return = [(key, self.time_series[key]) for key in dates_to_return]
|
||||
return TimeSeriesCore(data_to_return)
|
||||
return TimeSeriesCore(data_to_return, frequency=self.frequency.symbol)
|
||||
|
||||
if isinstance(key, int):
|
||||
raise KeyError(f"{key}. For index based slicing, use .iloc[{key}]")
|
||||
elif isinstance(key, datetime.datetime):
|
||||
elif isinstance(key, (datetime.datetime, datetime.date)):
|
||||
key = _parse_date(key)
|
||||
item = (key, self.time_series[key])
|
||||
if isinstance(key, str):
|
||||
if key == 'dates':
|
||||
elif isinstance(key, str):
|
||||
if key == "dates":
|
||||
return self.dates
|
||||
elif key == 'values':
|
||||
elif key == "values":
|
||||
return self.values
|
||||
try:
|
||||
dt_key = datetime.datetime.strptime(key, FincalOptions.date_format)
|
||||
item = (dt_key, self.time_series[dt_key])
|
||||
except ValueError:
|
||||
raise KeyError(f"{repr(key)}. If you passed a date as a string, "
|
||||
"try setting the date format using Fincal.Options.date_format")
|
||||
except KeyError:
|
||||
raise KeyError(f"{repr(key)}. This date is not available.")
|
||||
|
||||
dt_key = _parse_date(key)
|
||||
item = (dt_key, self.time_series[dt_key])
|
||||
|
||||
elif isinstance(key, Sequence):
|
||||
item = [(k, self.time_series[k]) for k in key]
|
||||
keys = [_parse_date(i) for i in key]
|
||||
item = [(k, self.time_series[k]) for k in keys]
|
||||
else:
|
||||
raise TypeError(f"Invalid type {repr(type(key).__name__)} for slicing.")
|
||||
return item
|
||||
|
Loading…
Reference in New Issue
Block a user