datedict 0.1.2__py3-none-any.whl → 0.1.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of datedict might be problematic. Click here for more details.
- datedict/DateDict.py +169 -92
- datedict/YearDict.py +115 -77
- datedict/__init__.py +2 -2
- datedict/common.py +7 -2
- {datedict-0.1.2.dist-info → datedict-0.1.3.dist-info}/METADATA +1 -1
- datedict-0.1.3.dist-info/RECORD +8 -0
- datedict-0.1.2.dist-info/RECORD +0 -8
- {datedict-0.1.2.dist-info → datedict-0.1.3.dist-info}/WHEEL +0 -0
- {datedict-0.1.2.dist-info → datedict-0.1.3.dist-info}/licenses/LICENSE +0 -0
datedict/DateDict.py
CHANGED
|
@@ -1,50 +1,72 @@
|
|
|
1
|
-
from datetime import date
|
|
1
|
+
from datetime import date, timedelta
|
|
2
2
|
from decimal import Decimal
|
|
3
|
+
from typing import Mapping
|
|
3
4
|
|
|
4
|
-
from
|
|
5
|
-
|
|
6
|
-
from .common import ZERO
|
|
5
|
+
from .common import NAN, ONE, Decimable, to_decimal, ZERO
|
|
7
6
|
|
|
8
7
|
|
|
9
8
|
class DateDict:
|
|
10
|
-
def __init__(self, data=
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
def __init__(self, data: Mapping[str, Decimable], strict: bool = True):
|
|
10
|
+
if not data:
|
|
11
|
+
raise ValueError("Data cannot be empty.")
|
|
12
|
+
dates = sorted(data.keys())
|
|
13
|
+
if strict:
|
|
14
|
+
# enforce contiguous coverage
|
|
15
|
+
if dates != [
|
|
16
|
+
(date.fromisoformat(dates[0]) + timedelta(days=i)).strftime("%Y-%m-%d")
|
|
17
|
+
for i in range(
|
|
18
|
+
(date.fromisoformat(dates[-1]) - date.fromisoformat(dates[0])).days
|
|
19
|
+
+ 1
|
|
20
|
+
)
|
|
21
|
+
]:
|
|
22
|
+
raise ValueError(
|
|
23
|
+
"Data must cover all dates in the contiguous range. "
|
|
24
|
+
"To disable this check, set strict=False."
|
|
25
|
+
)
|
|
26
|
+
self.start_date, self.end_date = dates[0], dates[-1]
|
|
27
|
+
self.data: dict[str, Decimal] = {k: to_decimal(v) for k, v in data.items()}
|
|
28
|
+
|
|
29
|
+
def fill(self, start_date, end_date, value: Decimable) -> "DateDict":
|
|
14
30
|
"""
|
|
15
31
|
Create a new graph with a specified range and value.
|
|
16
32
|
The range is defined by start_date and end_date.
|
|
17
|
-
If the value is None, it will not be included in the graph.
|
|
18
33
|
"""
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
34
|
+
v = to_decimal(value)
|
|
35
|
+
return DateDict(
|
|
36
|
+
{
|
|
37
|
+
(date.fromisoformat(start_date) + timedelta(days=i)).strftime(
|
|
38
|
+
"%Y-%m-%d"
|
|
39
|
+
): v
|
|
40
|
+
for i in range(
|
|
41
|
+
(date.fromisoformat(end_date) - date.fromisoformat(start_date)).days
|
|
42
|
+
+ 1
|
|
43
|
+
)
|
|
44
|
+
}
|
|
23
45
|
)
|
|
24
|
-
end = date.fromisoformat(end_date) if isinstance(end_date, str) else end_date
|
|
25
|
-
self.data = {
|
|
26
|
-
date.strftime("%Y-%m-%d"): (value if value is not None else None)
|
|
27
|
-
for date in (
|
|
28
|
-
start + timedelta(days=i) for i in range((end - start).days + 1)
|
|
29
|
-
)
|
|
30
|
-
}
|
|
31
|
-
return self
|
|
32
46
|
|
|
33
|
-
def get(self, key: str, default: Decimal) -> Decimal:
|
|
47
|
+
def get(self, key: str, default: Decimal = NAN) -> Decimal:
|
|
34
48
|
"""
|
|
35
49
|
Get the value for a specific date. If the date does not exist, return the default value.
|
|
36
50
|
The date should be in the format yyyy-mm-dd.
|
|
37
51
|
If the value is None, return the default value.
|
|
38
52
|
"""
|
|
39
|
-
|
|
53
|
+
temp = self.data.get(key, NAN)
|
|
54
|
+
if temp.is_nan():
|
|
55
|
+
return default
|
|
56
|
+
return temp
|
|
40
57
|
|
|
41
|
-
def __getitem__(self, key) -> Decimal
|
|
42
|
-
return self.data
|
|
58
|
+
def __getitem__(self, key) -> Decimal:
|
|
59
|
+
return self.data[key]
|
|
43
60
|
|
|
44
61
|
def __setitem__(self, key: str, value) -> None:
|
|
45
|
-
self.data[key] = value
|
|
46
|
-
|
|
47
|
-
def crop(
|
|
62
|
+
self.data[key] = to_decimal(value)
|
|
63
|
+
|
|
64
|
+
def crop(
|
|
65
|
+
self,
|
|
66
|
+
start: str | None = None,
|
|
67
|
+
end: str | None = None,
|
|
68
|
+
initial_value: Decimable = NAN,
|
|
69
|
+
) -> "DateDict":
|
|
48
70
|
"""
|
|
49
71
|
Crop the graph data to a specific range defined by start and end.
|
|
50
72
|
If any of the parameters is None, it will not filter by that parameter.
|
|
@@ -53,85 +75,140 @@ class DateDict:
|
|
|
53
75
|
return self
|
|
54
76
|
return DateDict(
|
|
55
77
|
{
|
|
56
|
-
k:
|
|
57
|
-
for k
|
|
58
|
-
|
|
78
|
+
k: (self.get(k, to_decimal(initial_value)))
|
|
79
|
+
for k in map(
|
|
80
|
+
lambda x: x.strftime("%Y-%m-%d"),
|
|
81
|
+
[
|
|
82
|
+
(
|
|
83
|
+
date.fromisoformat(self.start_date)
|
|
84
|
+
if start is None
|
|
85
|
+
else date.fromisoformat(start)
|
|
86
|
+
)
|
|
87
|
+
+ timedelta(days=i)
|
|
88
|
+
for i in range(
|
|
89
|
+
(
|
|
90
|
+
(
|
|
91
|
+
date.fromisoformat(self.end_date)
|
|
92
|
+
if end is None
|
|
93
|
+
else date.fromisoformat(end)
|
|
94
|
+
)
|
|
95
|
+
- (
|
|
96
|
+
date.fromisoformat(self.start_date)
|
|
97
|
+
if start is None
|
|
98
|
+
else date.fromisoformat(start)
|
|
99
|
+
)
|
|
100
|
+
).days
|
|
101
|
+
+ 1
|
|
102
|
+
)
|
|
103
|
+
],
|
|
104
|
+
)
|
|
59
105
|
}
|
|
60
106
|
)
|
|
61
107
|
|
|
62
|
-
def
|
|
108
|
+
def non_negative(self) -> "DateDict":
|
|
63
109
|
"""
|
|
64
|
-
|
|
65
|
-
If a value is None, it is treated as zero.
|
|
110
|
+
Return a new DateDict with all negative values set to zero.
|
|
66
111
|
"""
|
|
67
|
-
return
|
|
68
|
-
|
|
112
|
+
return DateDict({k: (v if (not v.is_nan() and v >= ZERO) else ZERO) for k, v in self.data.items()})
|
|
113
|
+
|
|
114
|
+
def sum(
|
|
115
|
+
self: "DateDict", start: str | None = None, end: str | None = None
|
|
116
|
+
) -> Decimal:
|
|
117
|
+
"""
|
|
118
|
+
Return the sum of values in the specified range.
|
|
119
|
+
If a value is NaN, it is treated as zero.
|
|
120
|
+
"""
|
|
121
|
+
s = self.start_date if start is None else start
|
|
122
|
+
e = self.end_date if end is None else end
|
|
123
|
+
return sum(
|
|
124
|
+
[
|
|
125
|
+
(self.get(k, ZERO))
|
|
126
|
+
for k in map(
|
|
127
|
+
lambda x: x.strftime("%Y-%m-%d"),
|
|
128
|
+
[
|
|
129
|
+
date.fromisoformat(s) + timedelta(days=i)
|
|
130
|
+
for i in range(
|
|
131
|
+
(date.fromisoformat(e) - date.fromisoformat(s)).days + 1
|
|
132
|
+
)
|
|
133
|
+
],
|
|
134
|
+
)
|
|
135
|
+
],
|
|
136
|
+
ZERO,
|
|
69
137
|
)
|
|
70
138
|
|
|
71
|
-
def
|
|
72
|
-
if isinstance(other,
|
|
73
|
-
return DateDict(
|
|
74
|
-
{
|
|
75
|
-
k: (v + other if v is not None else None)
|
|
76
|
-
for k, v in self.data.items()
|
|
77
|
-
}
|
|
78
|
-
)
|
|
79
|
-
else:
|
|
80
|
-
return DateDict(
|
|
81
|
-
{
|
|
82
|
-
k: ((v + (other.data.get(k) or ZERO) if v is not None else None))
|
|
83
|
-
for k, v in self.data.items()
|
|
84
|
-
}
|
|
85
|
-
)
|
|
86
|
-
|
|
87
|
-
def __mul__(self, other: "Decimal | DateDict") -> "DateDict":
|
|
88
|
-
if isinstance(other, Decimal):
|
|
89
|
-
return DateDict(
|
|
90
|
-
{
|
|
91
|
-
k: (v * other if v is not None else None)
|
|
92
|
-
for k, v in self.data.items()
|
|
93
|
-
}
|
|
94
|
-
)
|
|
95
|
-
elif isinstance(other, DateDict):
|
|
96
|
-
return DateDict(
|
|
97
|
-
{
|
|
98
|
-
k: ((v * (other.data.get(k) or ZERO) if v is not None else None))
|
|
99
|
-
for k, v in self.data.items()
|
|
100
|
-
}
|
|
101
|
-
)
|
|
102
|
-
|
|
103
|
-
def __sub__(self, other: "Decimal | DateDict") -> "DateDict":
|
|
104
|
-
return self + (other * Decimal(-1))
|
|
105
|
-
|
|
106
|
-
def __truediv__(self, other: "Decimal | DateDict") -> "DateDict":
|
|
107
|
-
if isinstance(other, Decimal):
|
|
108
|
-
return DateDict(
|
|
109
|
-
{
|
|
110
|
-
k: (v / other if v is not None else None)
|
|
111
|
-
for k, v in self.data.items()
|
|
112
|
-
}
|
|
113
|
-
)
|
|
139
|
+
def __mul__(self, other: "Decimable | DateDict") -> "DateDict":
|
|
140
|
+
if isinstance(other, Decimable):
|
|
141
|
+
return DateDict({k: v * to_decimal(other) for k, v in self.data.items()})
|
|
114
142
|
elif isinstance(other, DateDict):
|
|
115
|
-
return DateDict(
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
120
|
-
)
|
|
143
|
+
return DateDict({k: v * other.get(k, ONE) for k, v in self.data.items()})
|
|
144
|
+
|
|
145
|
+
def __rmul__(self, other: "Decimable | DateDict") -> "DateDict":
|
|
146
|
+
return self.__mul__(other)
|
|
121
147
|
|
|
122
|
-
def
|
|
148
|
+
def __neg__(self) -> "DateDict":
|
|
149
|
+
return self * Decimal("-1")
|
|
150
|
+
|
|
151
|
+
def __add__(self, other: "Decimable | DateDict") -> "DateDict":
|
|
152
|
+
if isinstance(other, Decimable):
|
|
153
|
+
return DateDict({k: (v + to_decimal(other)) for k, v in self.data.items()})
|
|
154
|
+
else:
|
|
155
|
+
return DateDict({k: v + other.get(k, ZERO) for k, v in self.data.items()})
|
|
156
|
+
|
|
157
|
+
def __radd__(self, other: "Decimable | DateDict") -> "DateDict":
|
|
158
|
+
return self.__add__(other)
|
|
159
|
+
|
|
160
|
+
def __sub__(self, other: "Decimable | DateDict") -> "DateDict":
|
|
161
|
+
if isinstance(other, Decimable):
|
|
162
|
+
return self.__add__(-to_decimal(other))
|
|
163
|
+
else:
|
|
164
|
+
return self.__add__(-other)
|
|
165
|
+
|
|
166
|
+
def __rsub__(self, other: "Decimable | DateDict") -> "DateDict":
|
|
167
|
+
return (-self).__add__(other)
|
|
168
|
+
|
|
169
|
+
def __truediv__(self, other: "Decimable | DateDict") -> "DateDict":
|
|
170
|
+
if isinstance(other, Decimable):
|
|
171
|
+
return DateDict({k: v / to_decimal(other) for k, v in self.data.items()})
|
|
172
|
+
else:
|
|
173
|
+
return DateDict({k: v / other.get(k, ONE) for k, v in self.data.items()})
|
|
174
|
+
|
|
175
|
+
def __eq__(self, other: object) -> bool:
|
|
176
|
+
if not isinstance(other, DateDict):
|
|
177
|
+
return False
|
|
178
|
+
for k in set(self.data.keys()).union(other.data.keys()):
|
|
179
|
+
s = self.get(k)
|
|
180
|
+
o = other.get(k)
|
|
181
|
+
if s.is_nan() and o.is_nan():
|
|
182
|
+
continue
|
|
183
|
+
if s != o:
|
|
184
|
+
return False
|
|
185
|
+
return True
|
|
186
|
+
|
|
187
|
+
def __str__(self) -> str:
|
|
188
|
+
return "\n".join(f"{k}: {v}" for k, v in sorted(self.data.items()))
|
|
189
|
+
|
|
190
|
+
def __repr__(self) -> str:
|
|
191
|
+
return f"{self.data!r}"
|
|
192
|
+
|
|
193
|
+
def to_array(self) -> list[Decimal]:
|
|
194
|
+
"""
|
|
195
|
+
Convert the DateDict values to a list of Decimals, ordered by date.
|
|
123
196
|
"""
|
|
124
|
-
|
|
125
|
-
|
|
197
|
+
return [self.data[k] for k in sorted(self.data.keys())]
|
|
198
|
+
|
|
199
|
+
def to_dict(self) -> dict[str, Decimal]:
|
|
200
|
+
"""
|
|
201
|
+
Convert the DateDict to a standard dictionary.
|
|
126
202
|
"""
|
|
127
|
-
return self.data
|
|
203
|
+
return dict(self.data)
|
|
204
|
+
|
|
128
205
|
|
|
129
206
|
def average(self) -> Decimal:
|
|
130
207
|
"""
|
|
131
|
-
Calculate the average of all values in the
|
|
132
|
-
If there are no valid values, return
|
|
208
|
+
Calculate the average of all values in the DateDict.
|
|
209
|
+
If there are no valid values, return ZERO.
|
|
133
210
|
"""
|
|
134
|
-
valid_values = [v for v in self.data.values() if
|
|
211
|
+
valid_values = [v for v in self.data.values() if not v.is_nan()]
|
|
135
212
|
if not valid_values:
|
|
136
213
|
return ZERO
|
|
137
214
|
return Decimal(sum(valid_values)) / len(valid_values)
|
datedict/YearDict.py
CHANGED
|
@@ -1,101 +1,129 @@
|
|
|
1
1
|
from decimal import Decimal
|
|
2
|
+
from typing import Mapping
|
|
2
3
|
|
|
3
|
-
from .common import to_decimal, ZERO
|
|
4
|
+
from .common import NAN, ONE, Decimable, to_decimal, ZERO
|
|
4
5
|
|
|
5
6
|
|
|
6
7
|
class YearDict:
|
|
7
|
-
def __init__(
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
8
|
+
def __init__(self, data: Mapping[int, Decimable], strict: bool = True):
|
|
9
|
+
if not data:
|
|
10
|
+
raise ValueError("Data cannot be empty.")
|
|
11
|
+
years = sorted(data.keys())
|
|
12
|
+
if strict:
|
|
13
|
+
# enforce contiguous coverage
|
|
14
|
+
if years != list(range(years[0], years[-1] + 1)):
|
|
15
|
+
raise ValueError(
|
|
16
|
+
"Data must cover all years in the contiguous range. "
|
|
17
|
+
"To disable this check, set strict=False."
|
|
18
|
+
)
|
|
19
|
+
self.start_year, self.end_year = years[0], years[-1]
|
|
20
|
+
self.data = {k: to_decimal(v) for k, v in data.items()}
|
|
21
|
+
|
|
22
|
+
def fill(self, start_year: int, end_year: int, value: Decimable) -> "YearDict":
|
|
23
|
+
"""
|
|
24
|
+
Create a new graph with a specified range and value.
|
|
25
|
+
The range is defined by start_year and end_year.
|
|
26
|
+
"""
|
|
27
|
+
v = to_decimal(value)
|
|
28
|
+
return YearDict({y: v for y in range(int(start_year), int(end_year) + 1)})
|
|
29
|
+
|
|
30
|
+
def get(self, year: int, default: Decimal = NAN) -> Decimal:
|
|
31
|
+
"""
|
|
32
|
+
Get the value for a specific year. If the year does not exist, return the default value.
|
|
33
|
+
If the value is None, return the default value.
|
|
34
|
+
"""
|
|
35
|
+
temp = self.data.get(year, NAN)
|
|
36
|
+
if temp.is_nan():
|
|
37
|
+
return default
|
|
38
|
+
return temp
|
|
19
39
|
|
|
20
40
|
def __getitem__(self, year: int) -> Decimal:
|
|
21
41
|
return self.data[year]
|
|
22
42
|
|
|
23
|
-
def __setitem__(self, year: int, value):
|
|
43
|
+
def __setitem__(self, year: int, value) -> None:
|
|
24
44
|
self.data[int(year)] = to_decimal(value)
|
|
25
45
|
|
|
26
|
-
def
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
for y in range(self.start_year, self.end_year + 1)
|
|
45
|
-
}
|
|
46
|
-
return self
|
|
46
|
+
def crop(
|
|
47
|
+
self,
|
|
48
|
+
start: int | None = None,
|
|
49
|
+
end: int | None = None,
|
|
50
|
+
initial_value: Decimable = NAN,
|
|
51
|
+
) -> "YearDict":
|
|
52
|
+
if start is None and end is None:
|
|
53
|
+
return self
|
|
54
|
+
return YearDict(
|
|
55
|
+
{
|
|
56
|
+
k: (self.get(k, to_decimal(initial_value)))
|
|
57
|
+
for k in range(
|
|
58
|
+
self.start_year if start is None else int(start),
|
|
59
|
+
self.end_year if end is None else int(end)
|
|
60
|
+
+1,
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
)
|
|
47
64
|
|
|
48
65
|
def non_negative(self) -> "YearDict":
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
66
|
+
"""
|
|
67
|
+
Return a new YearDict with all negative values set to zero.
|
|
68
|
+
"""
|
|
69
|
+
return YearDict({y: (v if (not v.is_nan() and v >= ZERO) else ZERO) for y, v in self.data.items()})
|
|
70
|
+
|
|
71
|
+
def sum(self, start: int | None = None, end: int | None = None) -> Decimal:
|
|
72
|
+
"""
|
|
73
|
+
Return the sum of values in the specified range.
|
|
74
|
+
If a value is NaN, it is treated as zero.
|
|
75
|
+
"""
|
|
76
|
+
s = self.start_year if start is None else start
|
|
77
|
+
e = self.end_year if end is None else end
|
|
78
|
+
return sum((self.get(y, ZERO) for y in range(s, e + 1) if y in self.data), ZERO)
|
|
79
|
+
|
|
80
|
+
def __mul__(self, other: "Decimable | YearDict") -> "YearDict":
|
|
81
|
+
if isinstance(other, Decimable):
|
|
82
|
+
return YearDict({k: v * to_decimal(other) for k, v in self.data.items()})
|
|
67
83
|
elif isinstance(other, YearDict):
|
|
68
|
-
|
|
69
|
-
year: Decimal(self.data[year]) * Decimal(other.data[year])
|
|
70
|
-
for year in self.data.keys() & other.data.keys()
|
|
71
|
-
}
|
|
72
|
-
else:
|
|
73
|
-
return NotImplemented
|
|
74
|
-
return result
|
|
84
|
+
return YearDict({k: v * other.get(k, ONE) for k, v in self.data.items()})
|
|
75
85
|
|
|
76
86
|
def __rmul__(self, other):
|
|
77
87
|
return self.__mul__(other)
|
|
78
88
|
|
|
79
|
-
def
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
89
|
+
def __neg__(self) -> "YearDict":
|
|
90
|
+
return self * Decimal("-1")
|
|
91
|
+
|
|
92
|
+
def __add__(self, other: "Decimable | YearDict") -> "YearDict":
|
|
93
|
+
if isinstance(other, Decimable):
|
|
94
|
+
return YearDict({k: v + to_decimal(other) for k, v in self.data.items()})
|
|
85
95
|
elif isinstance(other, YearDict):
|
|
86
|
-
|
|
87
|
-
year: Decimal(self.data[year]) + Decimal(other.data[year])
|
|
88
|
-
for year in self.data.keys() & other.data.keys()
|
|
89
|
-
}
|
|
90
|
-
else:
|
|
91
|
-
return NotImplemented
|
|
92
|
-
return result
|
|
96
|
+
return YearDict({k: v + other.get(k, ZERO) for k, v in self.data.items()})
|
|
93
97
|
|
|
94
|
-
def __radd__(self, other):
|
|
98
|
+
def __radd__(self, other: "Decimable | YearDict") -> "YearDict":
|
|
95
99
|
return self.__add__(other)
|
|
96
100
|
|
|
97
|
-
def __sub__(self, other):
|
|
98
|
-
|
|
101
|
+
def __sub__(self, other: "Decimable | YearDict") -> "YearDict":
|
|
102
|
+
if isinstance(other, Decimable):
|
|
103
|
+
return self.__add__(-to_decimal(other))
|
|
104
|
+
elif isinstance(other, YearDict):
|
|
105
|
+
return self.__add__(-other)
|
|
106
|
+
|
|
107
|
+
def __rsub__(self, other: "Decimable | YearDict") -> "YearDict":
|
|
108
|
+
return (-self).__add__(other)
|
|
109
|
+
|
|
110
|
+
def __truediv__(self, other: "Decimable | YearDict") -> "YearDict":
|
|
111
|
+
if isinstance(other, Decimable):
|
|
112
|
+
return YearDict({k: v / to_decimal(other) for k, v in self.data.items()})
|
|
113
|
+
elif isinstance(other, YearDict):
|
|
114
|
+
return YearDict({k: v / other.get(k, ONE) for k, v in self.data.items()})
|
|
115
|
+
|
|
116
|
+
def __eq__(self, other: object) -> bool:
|
|
117
|
+
if not isinstance(other, YearDict):
|
|
118
|
+
return False
|
|
119
|
+
for k in set(self.data.keys()).union(other.data.keys()):
|
|
120
|
+
s = self.get(k)
|
|
121
|
+
o = other.get(k)
|
|
122
|
+
if s.is_nan() and o.is_nan():
|
|
123
|
+
continue
|
|
124
|
+
if s != o:
|
|
125
|
+
return False
|
|
126
|
+
return True
|
|
99
127
|
|
|
100
128
|
def __str__(self):
|
|
101
129
|
return "\n".join(f"{y}: {v}" for y, v in sorted(self.data.items()))
|
|
@@ -104,7 +132,17 @@ class YearDict:
|
|
|
104
132
|
return f"{self.data!r}"
|
|
105
133
|
|
|
106
134
|
def to_array(self):
|
|
107
|
-
return [self.data[
|
|
135
|
+
return [self.data[k] for k in self.data.keys()]
|
|
108
136
|
|
|
109
137
|
def to_dict(self):
|
|
110
138
|
return dict(self.data)
|
|
139
|
+
|
|
140
|
+
def average(self) -> Decimal:
|
|
141
|
+
"""
|
|
142
|
+
Return the average of the values in the YearDict.
|
|
143
|
+
If there are no years, return Zero
|
|
144
|
+
"""
|
|
145
|
+
valid_values = [v for v in self.data.values() if not v.is_nan()]
|
|
146
|
+
if not valid_values:
|
|
147
|
+
return ZERO
|
|
148
|
+
return sum(valid_values, ZERO) / Decimal(len(valid_values))
|
datedict/__init__.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from .DateDict import DateDict
|
|
2
2
|
from .YearDict import YearDict
|
|
3
|
-
from .common import ZERO
|
|
3
|
+
from .common import ZERO, ONE, NAN, Decimable
|
|
4
4
|
|
|
5
|
-
__all__ = ["DateDict", "YearDict", "ZERO"]
|
|
5
|
+
__all__ = ["DateDict", "YearDict", "ZERO", "ONE", "NAN", "Decimable"]
|
|
6
6
|
__version__ = "0.1.2"
|
datedict/common.py
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
from decimal import Decimal
|
|
2
|
+
from typing import Union
|
|
2
3
|
|
|
4
|
+
Decimable = Union[Decimal, str, int, float, None]
|
|
3
5
|
|
|
4
6
|
ZERO = Decimal("0")
|
|
7
|
+
ONE = Decimal("1")
|
|
8
|
+
NAN = Decimal("NaN")
|
|
5
9
|
|
|
6
10
|
|
|
7
|
-
def to_decimal(x) -> Decimal:
|
|
11
|
+
def to_decimal(x: Decimable) -> Decimal:
|
|
12
|
+
if x is None:
|
|
13
|
+
return Decimal("NaN")
|
|
8
14
|
if isinstance(x, Decimal):
|
|
9
15
|
return x
|
|
10
16
|
if isinstance(x, (int, str)):
|
|
11
17
|
return Decimal(x)
|
|
12
18
|
if isinstance(x, float):
|
|
13
19
|
return Decimal(str(x))
|
|
14
|
-
raise TypeError(f"Unsupported type for Decimal: {type(x)}")
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
datedict/DateDict.py,sha256=YsvCgsNGHKDaCya8KEb3KrTo98cxoiXZrr6xqsz2Mq8,7700
|
|
2
|
+
datedict/YearDict.py,sha256=YI6lE1SXYoK4R7LKLGLt75_ISNg0snBEJ_Uk4E-0c6c,5436
|
|
3
|
+
datedict/__init__.py,sha256=4QSCqCW_owSqD3k3Oz1WAsB4p_vvzUlINroAxxFCEWQ,201
|
|
4
|
+
datedict/common.py,sha256=aUCsMV6roKGQIQXSFIJIN0cxAXIlK_YyRDWS1EIBu8E,424
|
|
5
|
+
datedict-0.1.3.dist-info/METADATA,sha256=YW0NObrsMdZrHZE_plT8MEhw2lRSwzT3yontyIV8D2g,1985
|
|
6
|
+
datedict-0.1.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
7
|
+
datedict-0.1.3.dist-info/licenses/LICENSE,sha256=ULDgLM2c4o9DLMR-VEkeyuB-DRWBFX7VunJmlWShWxQ,1072
|
|
8
|
+
datedict-0.1.3.dist-info/RECORD,,
|
datedict-0.1.2.dist-info/RECORD
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
datedict/DateDict.py,sha256=JrqKAJIcZL4Q7JMvzKPZQCzXGQ3wZAEkpWABAfHXLr0,4658
|
|
2
|
-
datedict/YearDict.py,sha256=P8t43vUyOtxxABtIKf1epAU7Ohqx2-V9e9t-PyqFEh0,3798
|
|
3
|
-
datedict/__init__.py,sha256=pZv27luenqTNoEm9jdodgCu3B5vnYdDgMYllFwCA2Sc,153
|
|
4
|
-
datedict/common.py,sha256=yW3ohkISis4FP6baN8QJFPYHCkpVyecip7AlFg49_Kw,314
|
|
5
|
-
datedict-0.1.2.dist-info/METADATA,sha256=p8BOciltez0gbtRl-UPsG0ihMtI4cSn5w8PBm1sNlwM,1985
|
|
6
|
-
datedict-0.1.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
7
|
-
datedict-0.1.2.dist-info/licenses/LICENSE,sha256=ULDgLM2c4o9DLMR-VEkeyuB-DRWBFX7VunJmlWShWxQ,1072
|
|
8
|
-
datedict-0.1.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|