datedict 0.1.2__py3-none-any.whl → 0.1.4__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 +172 -91
- datedict/YearDict.py +118 -76
- datedict/__init__.py +2 -2
- datedict/common.py +7 -2
- {datedict-0.1.2.dist-info → datedict-0.1.4.dist-info}/METADATA +4 -4
- datedict-0.1.4.dist-info/RECORD +8 -0
- {datedict-0.1.2.dist-info → datedict-0.1.4.dist-info}/licenses/LICENSE +1 -1
- datedict-0.1.2.dist-info/RECORD +0 -8
- {datedict-0.1.2.dist-info → datedict-0.1.4.dist-info}/WHEEL +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=dict()):
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
def __init__(self, data: Mapping[str, Decimable] = dict(), 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,144 @@ class DateDict:
|
|
|
53
75
|
return self
|
|
54
76
|
return DateDict(
|
|
55
77
|
{
|
|
56
|
-
k:
|
|
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
|
+
)
|
|
105
|
+
}
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
def non_negative(self) -> "DateDict":
|
|
109
|
+
"""
|
|
110
|
+
Return a new DateDict with all negative values set to zero.
|
|
111
|
+
"""
|
|
112
|
+
return DateDict(
|
|
113
|
+
{
|
|
114
|
+
k: (v if (not v.is_nan() and v >= ZERO) else ZERO)
|
|
57
115
|
for k, v in self.data.items()
|
|
58
|
-
if (start is None or k >= start) and (end is None or k <= end)
|
|
59
116
|
}
|
|
60
117
|
)
|
|
61
118
|
|
|
62
|
-
def sum(
|
|
119
|
+
def sum(
|
|
120
|
+
self: "DateDict", start: str | None = None, end: str | None = None
|
|
121
|
+
) -> Decimal:
|
|
63
122
|
"""
|
|
64
|
-
|
|
65
|
-
If a value is
|
|
123
|
+
Return the sum of values in the specified range.
|
|
124
|
+
If a value is NaN, it is treated as zero.
|
|
66
125
|
"""
|
|
67
|
-
|
|
68
|
-
|
|
126
|
+
s = self.start_date if start is None else start
|
|
127
|
+
e = self.end_date if end is None else end
|
|
128
|
+
return sum(
|
|
129
|
+
[
|
|
130
|
+
(self.get(k, ZERO))
|
|
131
|
+
for k in map(
|
|
132
|
+
lambda x: x.strftime("%Y-%m-%d"),
|
|
133
|
+
[
|
|
134
|
+
date.fromisoformat(s) + timedelta(days=i)
|
|
135
|
+
for i in range(
|
|
136
|
+
(date.fromisoformat(e) - date.fromisoformat(s)).days + 1
|
|
137
|
+
)
|
|
138
|
+
],
|
|
139
|
+
)
|
|
140
|
+
],
|
|
141
|
+
ZERO,
|
|
69
142
|
)
|
|
70
143
|
|
|
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
|
-
)
|
|
144
|
+
def __mul__(self, other: "Decimable | DateDict") -> "DateDict":
|
|
145
|
+
if isinstance(other, Decimable):
|
|
146
|
+
return DateDict({k: v * to_decimal(other) for k, v in self.data.items()})
|
|
95
147
|
elif isinstance(other, DateDict):
|
|
96
|
-
return DateDict(
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
return DateDict(
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
return
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
148
|
+
return DateDict({k: v * other.get(k, ONE) for k, v in self.data.items()})
|
|
149
|
+
|
|
150
|
+
def __rmul__(self, other: "Decimable | DateDict") -> "DateDict":
|
|
151
|
+
return self.__mul__(other)
|
|
152
|
+
|
|
153
|
+
def __neg__(self) -> "DateDict":
|
|
154
|
+
return self * Decimal("-1")
|
|
155
|
+
|
|
156
|
+
def __add__(self, other: "Decimable | DateDict") -> "DateDict":
|
|
157
|
+
if isinstance(other, Decimable):
|
|
158
|
+
return DateDict({k: (v + to_decimal(other)) for k, v in self.data.items()})
|
|
159
|
+
else:
|
|
160
|
+
return DateDict({k: v + other.get(k, ZERO) for k, v in self.data.items()})
|
|
161
|
+
|
|
162
|
+
def __radd__(self, other: "Decimable | DateDict") -> "DateDict":
|
|
163
|
+
return self.__add__(other)
|
|
164
|
+
|
|
165
|
+
def __sub__(self, other: "Decimable | DateDict") -> "DateDict":
|
|
166
|
+
if isinstance(other, Decimable):
|
|
167
|
+
return self.__add__(-to_decimal(other))
|
|
168
|
+
else:
|
|
169
|
+
return self.__add__(-other)
|
|
170
|
+
|
|
171
|
+
def __rsub__(self, other: "Decimable | DateDict") -> "DateDict":
|
|
172
|
+
return (-self).__add__(other)
|
|
173
|
+
|
|
174
|
+
def __truediv__(self, other: "Decimable | DateDict") -> "DateDict":
|
|
175
|
+
if isinstance(other, Decimable):
|
|
176
|
+
return DateDict({k: v / to_decimal(other) for k, v in self.data.items()})
|
|
177
|
+
else:
|
|
178
|
+
return DateDict({k: v / other.get(k, ONE) for k, v in self.data.items()})
|
|
179
|
+
|
|
180
|
+
def __eq__(self, other: object) -> bool:
|
|
181
|
+
if not isinstance(other, DateDict):
|
|
182
|
+
return False
|
|
183
|
+
for k in set(self.data.keys()).union(other.data.keys()):
|
|
184
|
+
s = self.get(k)
|
|
185
|
+
o = other.get(k)
|
|
186
|
+
if s.is_nan() and o.is_nan():
|
|
187
|
+
continue
|
|
188
|
+
if s != o:
|
|
189
|
+
return False
|
|
190
|
+
return True
|
|
191
|
+
|
|
192
|
+
def __str__(self) -> str:
|
|
193
|
+
return "\n".join(f"{k}: {v}" for k, v in sorted(self.data.items()))
|
|
194
|
+
|
|
195
|
+
def __repr__(self) -> str:
|
|
196
|
+
return f"{self.data!r}"
|
|
197
|
+
|
|
198
|
+
def to_array(self) -> list[Decimal]:
|
|
199
|
+
"""
|
|
200
|
+
Convert the DateDict values to a list of Decimals, ordered by date.
|
|
201
|
+
"""
|
|
202
|
+
return [self.data[k] for k in sorted(self.data.keys())]
|
|
121
203
|
|
|
122
|
-
def to_dict(self) -> dict[str, Decimal
|
|
204
|
+
def to_dict(self) -> dict[str, Decimal]:
|
|
123
205
|
"""
|
|
124
|
-
Convert the
|
|
125
|
-
This is useful for serialization or returning as a response.
|
|
206
|
+
Convert the DateDict to a standard dictionary.
|
|
126
207
|
"""
|
|
127
|
-
return self.data
|
|
208
|
+
return dict(self.data)
|
|
128
209
|
|
|
129
210
|
def average(self) -> Decimal:
|
|
130
211
|
"""
|
|
131
|
-
Calculate the average of all values in the
|
|
132
|
-
If there are no valid values, return
|
|
212
|
+
Calculate the average of all values in the DateDict.
|
|
213
|
+
If there are no valid values, return ZERO.
|
|
133
214
|
"""
|
|
134
|
-
valid_values = [v for v in self.data.values() if
|
|
215
|
+
valid_values = [v for v in self.data.values() if not v.is_nan()]
|
|
135
216
|
if not valid_values:
|
|
136
217
|
return ZERO
|
|
137
218
|
return Decimal(sum(valid_values)) / len(valid_values)
|
datedict/YearDict.py
CHANGED
|
@@ -1,101 +1,133 @@
|
|
|
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] = dict(), 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
|
-
y: to_decimal(self.data[y]) if y in self.data else iv
|
|
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) + 1,
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
)
|
|
47
63
|
|
|
48
64
|
def non_negative(self) -> "YearDict":
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
sy = self.start_year if start_year is None else int(start_year)
|
|
57
|
-
ey = self.end_year if end_year is None else int(end_year)
|
|
58
|
-
return sum((self.data[y] for y in range(sy, ey + 1) if y in self.data), ZERO)
|
|
59
|
-
|
|
60
|
-
def __mul__(self, other):
|
|
61
|
-
result = YearDict(self.start_year, self.end_year)
|
|
62
|
-
if isinstance(other, (int, float, Decimal)):
|
|
63
|
-
result.data = {
|
|
64
|
-
year: Decimal(value) * Decimal(str(other))
|
|
65
|
-
for year, value in self.data.items()
|
|
65
|
+
"""
|
|
66
|
+
Return a new YearDict with all negative values set to zero.
|
|
67
|
+
"""
|
|
68
|
+
return YearDict(
|
|
69
|
+
{
|
|
70
|
+
y: (v if (not v.is_nan() and v >= ZERO) else ZERO)
|
|
71
|
+
for y, v in self.data.items()
|
|
66
72
|
}
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
def sum(self, start: int | None = None, end: int | None = None) -> Decimal:
|
|
76
|
+
"""
|
|
77
|
+
Return the sum of values in the specified range.
|
|
78
|
+
If a value is NaN, it is treated as zero.
|
|
79
|
+
"""
|
|
80
|
+
s = self.start_year if start is None else start
|
|
81
|
+
e = self.end_year if end is None else end
|
|
82
|
+
return sum((self.get(y, ZERO) for y in range(s, e + 1) if y in self.data), ZERO)
|
|
83
|
+
|
|
84
|
+
def __mul__(self, other: "Decimable | YearDict") -> "YearDict":
|
|
85
|
+
if isinstance(other, Decimable):
|
|
86
|
+
return YearDict({k: v * to_decimal(other) for k, v in self.data.items()})
|
|
67
87
|
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
|
|
88
|
+
return YearDict({k: v * other.get(k, ONE) for k, v in self.data.items()})
|
|
75
89
|
|
|
76
90
|
def __rmul__(self, other):
|
|
77
91
|
return self.__mul__(other)
|
|
78
92
|
|
|
79
|
-
def
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
93
|
+
def __neg__(self) -> "YearDict":
|
|
94
|
+
return self * Decimal("-1")
|
|
95
|
+
|
|
96
|
+
def __add__(self, other: "Decimable | YearDict") -> "YearDict":
|
|
97
|
+
if isinstance(other, Decimable):
|
|
98
|
+
return YearDict({k: v + to_decimal(other) for k, v in self.data.items()})
|
|
85
99
|
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
|
|
100
|
+
return YearDict({k: v + other.get(k, ZERO) for k, v in self.data.items()})
|
|
93
101
|
|
|
94
|
-
def __radd__(self, other):
|
|
102
|
+
def __radd__(self, other: "Decimable | YearDict") -> "YearDict":
|
|
95
103
|
return self.__add__(other)
|
|
96
104
|
|
|
97
|
-
def __sub__(self, other):
|
|
98
|
-
|
|
105
|
+
def __sub__(self, other: "Decimable | YearDict") -> "YearDict":
|
|
106
|
+
if isinstance(other, Decimable):
|
|
107
|
+
return self.__add__(-to_decimal(other))
|
|
108
|
+
elif isinstance(other, YearDict):
|
|
109
|
+
return self.__add__(-other)
|
|
110
|
+
|
|
111
|
+
def __rsub__(self, other: "Decimable | YearDict") -> "YearDict":
|
|
112
|
+
return (-self).__add__(other)
|
|
113
|
+
|
|
114
|
+
def __truediv__(self, other: "Decimable | YearDict") -> "YearDict":
|
|
115
|
+
if isinstance(other, Decimable):
|
|
116
|
+
return YearDict({k: v / to_decimal(other) for k, v in self.data.items()})
|
|
117
|
+
elif isinstance(other, YearDict):
|
|
118
|
+
return YearDict({k: v / other.get(k, ONE) for k, v in self.data.items()})
|
|
119
|
+
|
|
120
|
+
def __eq__(self, other: object) -> bool:
|
|
121
|
+
if not isinstance(other, YearDict):
|
|
122
|
+
return False
|
|
123
|
+
for k in set(self.data.keys()).union(other.data.keys()):
|
|
124
|
+
s = self.get(k)
|
|
125
|
+
o = other.get(k)
|
|
126
|
+
if s.is_nan() and o.is_nan():
|
|
127
|
+
continue
|
|
128
|
+
if s != o:
|
|
129
|
+
return False
|
|
130
|
+
return True
|
|
99
131
|
|
|
100
132
|
def __str__(self):
|
|
101
133
|
return "\n".join(f"{y}: {v}" for y, v in sorted(self.data.items()))
|
|
@@ -104,7 +136,17 @@ class YearDict:
|
|
|
104
136
|
return f"{self.data!r}"
|
|
105
137
|
|
|
106
138
|
def to_array(self):
|
|
107
|
-
return [self.data[
|
|
139
|
+
return [self.data[k] for k in self.data.keys()]
|
|
108
140
|
|
|
109
141
|
def to_dict(self):
|
|
110
142
|
return dict(self.data)
|
|
143
|
+
|
|
144
|
+
def average(self) -> Decimal:
|
|
145
|
+
"""
|
|
146
|
+
Return the average of the values in the YearDict.
|
|
147
|
+
If there are no years, return Zero
|
|
148
|
+
"""
|
|
149
|
+
valid_values = [v for v in self.data.values() if not v.is_nan()]
|
|
150
|
+
if not valid_values:
|
|
151
|
+
return ZERO
|
|
152
|
+
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)}")
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: datedict
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: DateDict and YearDict: date-aware dictionary structures
|
|
5
|
-
Project-URL: Homepage, https://
|
|
6
|
-
Project-URL: Issues, https://
|
|
5
|
+
Project-URL: Homepage, https://gitlab.com/grisus/datedict
|
|
6
|
+
Project-URL: Issues, https://gitlab.com/grisus/datedict/-/issues
|
|
7
7
|
Author-email: Lorenzo Guideri <lorenzo.guideri@dec-energy.ch>
|
|
8
8
|
License: MIT License
|
|
9
9
|
|
|
10
|
-
Copyright (c) 2025
|
|
10
|
+
Copyright (c) 2025 DEC Energy SA
|
|
11
11
|
|
|
12
12
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
13
13
|
of this software and associated documentation files (the "Software"), to deal
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
datedict/DateDict.py,sha256=2UdkSp-dVFqmbFMWnlK7Ol1Gixv2koiwjWw7tw79Zhs,7776
|
|
2
|
+
datedict/YearDict.py,sha256=EcVg4y8Tzfgr33KjCIdoKP4GUN1GDVOrxGpHDH0ZLGQ,5494
|
|
3
|
+
datedict/__init__.py,sha256=4QSCqCW_owSqD3k3Oz1WAsB4p_vvzUlINroAxxFCEWQ,201
|
|
4
|
+
datedict/common.py,sha256=aUCsMV6roKGQIQXSFIJIN0cxAXIlK_YyRDWS1EIBu8E,424
|
|
5
|
+
datedict-0.1.4.dist-info/METADATA,sha256=-SL17egrcvdh_MkOPa84wvJyFUeRNQ88tlw2tvYuYwE,1991
|
|
6
|
+
datedict-0.1.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
7
|
+
datedict-0.1.4.dist-info/licenses/LICENSE,sha256=GMmiaNfJSsljmGtIp9lCY6V-Dkc9NDZn0jSZuAbFe7Y,1070
|
|
8
|
+
datedict-0.1.4.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
|