Skip to content

Commit c312a0d

Browse files
committed
Start work on representing scale of NumRepr of DateTime
Excel works on days, UNIX on seconds, etc. etc.
1 parent 43b8c00 commit c312a0d

2 files changed

Lines changed: 64 additions & 29 deletions

File tree

structa/types.py

Lines changed: 56 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
import math
88
from copy import copy
99
from numbers import Real
10-
from datetime import datetime
1110
from textwrap import indent, shorten
11+
from datetime import datetime, timedelta
1212
from functools import partial, total_ordering
1313
from collections.abc import Mapping
1414
from operator import attrgetter
@@ -905,12 +905,14 @@ def from_strings(cls, iterable, pattern, bad_threshold=0):
905905
pattern=pattern)
906906

907907
@classmethod
908-
def from_numbers(cls, pattern, epoch=None):
908+
def from_numbers(cls, pattern, epoch=datetime.utcfromtimestamp(0),
909+
unit=timedelta(seconds=1)):
909910
"""
910911
Class method for constructing an instance wrapped in a :class:`NumRepr`
911912
to indicate a numeric representation of a set of timestamps (e.g. day
912913
offset from the UNIX epoch; a different *epoch* may be specified as
913-
a :class:`~datetime.datetime`).
914+
a :class:`~datetime.datetime`, and a different *unit* as a
915+
:class:`~datetime.timedelta`, which defaults to 1 second).
914916
915917
Constructed with an *sample* of number, a *pattern* (which can be a
916918
:class:`StrRepr` instance if the numbers are themselves represented as
@@ -923,14 +925,14 @@ def from_numbers(cls, pattern, epoch=None):
923925
else:
924926
num_pattern = pattern
925927
dt_counter = Counter()
926-
if epoch is None:
927-
offset = 0
928-
else:
929-
unix_epoch = datetime.utcfromtimestamp(0)
930-
offset = (unix_epoch - epoch).total_seconds() / 86400
928+
unix_epoch = datetime.utcfromtimestamp(0)
929+
offset = (epoch - unix_epoch).total_seconds()
930+
scale = unit.total_seconds()
931931
for value, count in num_pattern.values.sample.items():
932-
dt_counter[datetime.utcfromtimestamp(value - offset)] = count
933-
result = NumRepr(cls(dt_counter), pattern=num_pattern.__class__)
932+
dt_value = datetime.utcfromtimestamp((value * scale) + offset)
933+
dt_counter[dt_value] = count
934+
result = NumRepr(cls(dt_counter), pattern=(
935+
num_pattern.__class__, scale, offset))
934936
if isinstance(pattern, StrRepr):
935937
return pattern.with_content(result)
936938
else:
@@ -1205,36 +1207,65 @@ class NumRepr(Repr):
12051207
__slots__ = ()
12061208

12071209
def __str__(self):
1208-
if self.pattern is Int:
1209-
template = 'int of {self.content}'
1210-
elif self.pattern is Float:
1211-
template = 'float of {self.content}'
1210+
type_, scale, offset = self.pattern
1211+
delta = timedelta(seconds=scale)
1212+
unit = ', '.join(
1213+
'{value}{prop}'.format(
1214+
value='{value}*' if value != 1 else '',
1215+
prop=prop)
1216+
for prop in ('days', 'seconds', 'microseconds')
1217+
for value in (getattr(delta, prop),)
1218+
)
1219+
if not epoch % 86400:
1220+
epoch = datetime.utcfromtimestamp(offset).date().isoformat()
12121221
else:
1213-
assert False, 'str(num-repr) of {self.content!r}'.format(self=self)
1214-
return template.format(self=self)
1222+
epoch = datetime.utcfromtimestamp(offset).isoformat()
1223+
if type_ is Int:
1224+
template = 'int {unit} after {epoch} of {self.content}'
1225+
elif type_ is Float:
1226+
template = 'float {unit} after {epoch} of {self.content}'
1227+
else:
1228+
assert False, 'str(num-repr) of {self.content!r}'.format(
1229+
self=self, unit=unit, epoch=epoch)
1230+
return template.format(self=self, unit=unit, epoch=epoch)
12151231

12161232
def __xml__(self):
1217-
if self.pattern is Int:
1218-
return tag.intof(xml(self.content))
1219-
elif self.pattern is Float:
1220-
return tag.floatof(xml(self.content))
1233+
type_, scale, offset = self.pattern
1234+
if type_ is Int:
1235+
return tag.intof(xml(self.content), scale=scale, offset=offset)
1236+
elif type_ is Float:
1237+
return tag.floatof(xml(self.content), scale=scale, offset=offset)
12211238
else:
12221239
assert False, 'xml(num-repr) of {self.content!r}'.format(self=self)
12231240

12241241
def __add__(self, other):
12251242
if self == other:
1226-
if self.pattern is Float or other.pattern is Float:
1227-
pattern = Float
1243+
self_type, self_scale, self_offset = self.pattern
1244+
other_type, other_scale, other_offset = other.pattern
1245+
if self_type is Float or other_type is Float:
1246+
add_type = Float
12281247
else:
1229-
pattern = Int
1230-
return NumRepr(self.content + other.content, pattern)
1248+
add_type = Int
1249+
return NumRepr(
1250+
self.content + other.content,
1251+
(add_type, self_scale, self_offset))
12311252
return NotImplemented
12321253

1254+
def __eq__(self, other):
1255+
if not isinstance(other, NumRepr):
1256+
return NotImplemented
1257+
if super().__eq__(other) is not True:
1258+
return False
1259+
self_type, self_scale, self_offset = self.pattern
1260+
other_type, other_scale, other_offset = other.pattern
1261+
return (self_scale == other_scale) and (self_offset == other_offset)
1262+
12331263
def validate(self, value):
12341264
if not isinstance(value, Real):
12351265
raise TypeError('{value!r} is not a number'.format(value=value))
12361266
if isinstance(self.content, DateTime):
1237-
value = datetime.utcfromtimestamp(value)
1267+
type_, scale, offset = self.pattern
1268+
value = datetime.utcfromtimestamp((value * scale) + offset)
12381269
else:
12391270
assert False, (
12401271
'validating num-repr of {self.content!r}'.format(self=self))

tests/test_types.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -821,15 +821,19 @@ def test_datetime_numrepr():
821821
@pytest.mark.skipif(sys.maxsize <= 2**32, reason="requires 64-bit arch")
822822
def test_datetime_numrepr_epoch():
823823
excel_epoch = dt.datetime(1899, 12, 30)
824-
offset = (dt.datetime.utcfromtimestamp(0) - excel_epoch).total_seconds() // 86400
824+
base_epoch = dt.datetime.utcfromtimestamp(0)
825+
offset = (excel_epoch - base_epoch).total_seconds()
826+
scale = dt.timedelta(days=1).total_seconds()
825827
data = {
826828
dt.datetime(1943, 7, 20),
827829
dt.datetime(1970, 1, 1),
828830
dt.datetime(1976, 1, 1),
829831
}
830-
numbers = Int(Counter(d.timestamp() + offset for d in data))
831-
pattern = DateTime.from_numbers(numbers, epoch=excel_epoch)
832-
assert pattern == NumRepr(DateTime(Counter(data)), pattern=Int)
832+
numbers = Int(Counter((d.timestamp() - offset) // scale for d in data))
833+
pattern = DateTime.from_numbers(numbers, epoch=excel_epoch,
834+
unit=dt.timedelta(days=1))
835+
assert pattern == NumRepr(DateTime(Counter(data)),
836+
pattern=(Int, offset, 86400))
833837
pattern.validate(1000)
834838
with pytest.raises(TypeError):
835839
pattern.validate('1000')

0 commit comments

Comments
 (0)