morphir-python 0.0.1__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.
- morphir/__init__.py +14 -0
- morphir/ir/__init__.py +110 -0
- morphir/ir/access_controlled.py +15 -0
- morphir/ir/decorations.py +19 -0
- morphir/ir/diagnostics.py +105 -0
- morphir/ir/distribution.py +21 -0
- morphir/ir/document.py +37 -0
- morphir/ir/documented.py +11 -0
- morphir/ir/format_version.py +483 -0
- morphir/ir/fqname.py +9 -0
- morphir/ir/json.py +11 -0
- morphir/ir/literal.py +25 -0
- morphir/ir/meta.py +11 -0
- morphir/ir/migrate.py +1267 -0
- morphir/ir/module.py +17 -0
- morphir/ir/name.py +27 -0
- morphir/ir/package.py +15 -0
- morphir/ir/path.py +25 -0
- morphir/ir/qname.py +9 -0
- morphir/ir/ref.py +17 -0
- morphir/ir/type.py +37 -0
- morphir/ir/type_constraints.py +33 -0
- morphir/ir/type_def.py +19 -0
- morphir/ir/type_spec.py +25 -0
- morphir/ir/v3/__init__.py +64 -0
- morphir/ir/v3/_legacy_value.py +201 -0
- morphir/ir/v3/access_controlled.py +31 -0
- morphir/ir/v3/codec.py +1275 -0
- morphir/ir/v3/codec_v1.py +491 -0
- morphir/ir/v3/codec_v2.py +187 -0
- morphir/ir/v3/distribution.py +42 -0
- morphir/ir/v3/documented.py +13 -0
- morphir/ir/v3/fqname.py +30 -0
- morphir/ir/v3/literal.py +46 -0
- morphir/ir/v3/module.py +32 -0
- morphir/ir/v3/name.py +42 -0
- morphir/ir/v3/package.py +23 -0
- morphir/ir/v3/path.py +29 -0
- morphir/ir/v3/qname.py +16 -0
- morphir/ir/v3/type.py +126 -0
- morphir/ir/v3/value.py +235 -0
- morphir/ir/v4/__init__.py +160 -0
- morphir/ir/v4/access_controlled.py +17 -0
- morphir/ir/v4/annotation.py +75 -0
- morphir/ir/v4/codec.py +2161 -0
- morphir/ir/v4/decorations.py +88 -0
- morphir/ir/v4/distribution.py +56 -0
- morphir/ir/v4/document.py +69 -0
- morphir/ir/v4/documented.py +10 -0
- morphir/ir/v4/fqname.py +69 -0
- morphir/ir/v4/incompleteness.py +78 -0
- morphir/ir/v4/json.py +103 -0
- morphir/ir/v4/literal.py +51 -0
- morphir/ir/v4/meta.py +27 -0
- morphir/ir/v4/module.py +30 -0
- morphir/ir/v4/name.py +69 -0
- morphir/ir/v4/package.py +19 -0
- morphir/ir/v4/path.py +50 -0
- morphir/ir/v4/qname.py +34 -0
- morphir/ir/v4/ref.py +35 -0
- morphir/ir/v4/type.py +140 -0
- morphir/ir/v4/type_constraints.py +91 -0
- morphir/ir/v4/type_def.py +38 -0
- morphir/ir/v4/type_spec.py +64 -0
- morphir/ir/v4/value.py +480 -0
- morphir/ir/value.py +81 -0
- morphir/py.typed +0 -0
- morphir/sdk/__init__.py +110 -0
- morphir/sdk/_compare.py +187 -0
- morphir/sdk/aggregate.py +421 -0
- morphir/sdk/basics.py +504 -0
- morphir/sdk/char.py +110 -0
- morphir/sdk/decimal.py +311 -0
- morphir/sdk/dict.py +336 -0
- morphir/sdk/instant.py +118 -0
- morphir/sdk/int.py +86 -0
- morphir/sdk/key.py +522 -0
- morphir/sdk/list.py +356 -0
- morphir/sdk/local_date.py +436 -0
- morphir/sdk/local_time.py +267 -0
- morphir/sdk/maybe.py +135 -0
- morphir/sdk/number.py +237 -0
- morphir/sdk/regex.py +287 -0
- morphir/sdk/result.py +162 -0
- morphir/sdk/result_list.py +169 -0
- morphir/sdk/rule.py +103 -0
- morphir/sdk/set.py +214 -0
- morphir/sdk/stateful_app.py +49 -0
- morphir/sdk/string.py +427 -0
- morphir/sdk/tuple.py +43 -0
- morphir/sdk/uuid.py +209 -0
- morphir_python-0.0.1.dist-info/METADATA +269 -0
- morphir_python-0.0.1.dist-info/RECORD +99 -0
- morphir_python-0.0.1.dist-info/WHEEL +4 -0
- morphir_python-0.0.1.dist-info/entry_points.txt +2 -0
- morphir_python-0.0.1.dist-info/licenses/LICENSE +201 -0
- morphir_tools/__init__.py +13 -0
- morphir_tools/cli.py +223 -0
- morphir_tools/py.typed +0 -0
morphir/sdk/_compare.py
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""Ordering and structural equality shared by the SDK modules.
|
|
2
|
+
|
|
3
|
+
Elm's `compare` works on the comparable types only: `Int`, `Float`, `Char`,
|
|
4
|
+
`String`, and tuples or lists of comparables. The Elm type checker rejects
|
|
5
|
+
anything else. Python has no such check, so `compare` raises `TypeError` at run
|
|
6
|
+
time instead.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import dataclasses
|
|
10
|
+
from enum import Enum
|
|
11
|
+
from functools import cmp_to_key
|
|
12
|
+
from typing import TYPE_CHECKING, Any
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from collections.abc import Callable, Sequence
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Order(Enum):
|
|
19
|
+
"""The result of a comparison: less than, equal or greater than."""
|
|
20
|
+
|
|
21
|
+
LT = "LT"
|
|
22
|
+
EQ = "EQ"
|
|
23
|
+
GT = "GT"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
type Comparable = int | float | str | Sequence[Comparable]
|
|
27
|
+
"""The Elm comparable types: numbers, strings, and sequences of comparables."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _is_number(value: object) -> bool:
|
|
31
|
+
return isinstance(value, (int, float)) and not isinstance(value, bool)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _order(a: Any, b: Any) -> Order:
|
|
35
|
+
if a < b:
|
|
36
|
+
return Order.LT
|
|
37
|
+
if a > b:
|
|
38
|
+
return Order.GT
|
|
39
|
+
return Order.EQ
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def compare(a: object, b: object) -> Order:
|
|
43
|
+
"""Compare two comparable values.
|
|
44
|
+
|
|
45
|
+
Numbers compare by value and strings by code point. Tuples and lists compare
|
|
46
|
+
element by element; when one is a prefix of the other, the shorter one is
|
|
47
|
+
less.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
a: The first value.
|
|
51
|
+
b: The second value.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
`Order.LT`, `Order.EQ` or `Order.GT`.
|
|
55
|
+
|
|
56
|
+
Raises:
|
|
57
|
+
TypeError: If the values are not comparable, or not of the same kind
|
|
58
|
+
(for example an `int` and a `str`, or two `bool` values).
|
|
59
|
+
"""
|
|
60
|
+
if _is_number(a) and _is_number(b):
|
|
61
|
+
return _order(a, b)
|
|
62
|
+
if isinstance(a, str) and isinstance(b, str):
|
|
63
|
+
return _order(a, b)
|
|
64
|
+
if isinstance(a, (tuple, list)) and isinstance(b, (tuple, list)):
|
|
65
|
+
for x, y in zip(a, b, strict=False):
|
|
66
|
+
order = compare(x, y)
|
|
67
|
+
if order is not Order.EQ:
|
|
68
|
+
return order
|
|
69
|
+
return _order(len(a), len(b))
|
|
70
|
+
raise TypeError(
|
|
71
|
+
f"compare: values are not comparable "
|
|
72
|
+
f"({type(a).__name__} and {type(b).__name__})"
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def to_int(order: Order) -> int:
|
|
77
|
+
"""Convert an `Order` to the -1, 0 or 1 that Python sort functions expect."""
|
|
78
|
+
if order is Order.LT:
|
|
79
|
+
return -1
|
|
80
|
+
if order is Order.GT:
|
|
81
|
+
return 1
|
|
82
|
+
return 0
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def sort_key[A](by: Callable[[A, A], Order]) -> Callable[[A], Any]:
|
|
86
|
+
"""Build a `key` function for `sorted` from a function that yields an `Order`."""
|
|
87
|
+
return cmp_to_key(lambda a, b: to_int(by(a, b)))
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def search[K: Comparable, T](
|
|
91
|
+
key: K, items: Sequence[T], key_of: Callable[[T], K]
|
|
92
|
+
) -> tuple[bool, int]:
|
|
93
|
+
"""Find a key in a sequence of items sorted by key in ascending order.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
key: The key to find.
|
|
97
|
+
items: The sorted items, with no key twice.
|
|
98
|
+
key_of: A function that gives the key of an item.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
A pair `(found, index)`. When `found` is true, `index` is the position
|
|
102
|
+
of the item with the key. Otherwise `index` is the position where the
|
|
103
|
+
key must go to keep the sequence sorted.
|
|
104
|
+
|
|
105
|
+
Raises:
|
|
106
|
+
TypeError: If the key is not comparable with the keys in the sequence.
|
|
107
|
+
"""
|
|
108
|
+
low = 0
|
|
109
|
+
high = len(items)
|
|
110
|
+
while low < high:
|
|
111
|
+
middle = (low + high) // 2
|
|
112
|
+
order = compare(key_of(items[middle]), key)
|
|
113
|
+
if order is Order.EQ:
|
|
114
|
+
return (True, middle)
|
|
115
|
+
if order is Order.LT:
|
|
116
|
+
low = middle + 1
|
|
117
|
+
else:
|
|
118
|
+
high = middle
|
|
119
|
+
return (False, low)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def freeze(value: Any) -> Any:
|
|
123
|
+
"""Turn every list inside a comparable value into a tuple.
|
|
124
|
+
|
|
125
|
+
`Dict` and `Set` keep their keys in sorted order. A list key that changed
|
|
126
|
+
after it was stored would break that order, and a list cannot be hashed. A
|
|
127
|
+
stored key is therefore always the tuple form. A value with no list in it is
|
|
128
|
+
returned as it is.
|
|
129
|
+
"""
|
|
130
|
+
if isinstance(value, list):
|
|
131
|
+
return tuple(freeze(item) for item in value)
|
|
132
|
+
if isinstance(value, tuple):
|
|
133
|
+
frozen = tuple(freeze(item) for item in value)
|
|
134
|
+
if all(new is old for new, old in zip(frozen, value, strict=True)):
|
|
135
|
+
return value
|
|
136
|
+
return frozen
|
|
137
|
+
return value
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def check_strictly_ascending(keys: Sequence[Any], owner: str) -> None:
|
|
141
|
+
"""Check that keys are sorted in ascending order and that none repeats.
|
|
142
|
+
|
|
143
|
+
Raises:
|
|
144
|
+
ValueError: If a key is not greater than the key before it.
|
|
145
|
+
TypeError: If two keys are not comparable with each other.
|
|
146
|
+
"""
|
|
147
|
+
for index in range(1, len(keys)):
|
|
148
|
+
if compare(keys[index - 1], keys[index]) is not Order.LT:
|
|
149
|
+
raise ValueError(
|
|
150
|
+
f"{owner}: entries must be sorted by key with no key twice; "
|
|
151
|
+
f"use from_list to build one from entries in any order"
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _contains_function(value: object) -> bool:
|
|
156
|
+
if isinstance(value, (tuple, list)):
|
|
157
|
+
return any(_contains_function(item) for item in value)
|
|
158
|
+
if dataclasses.is_dataclass(value) and not isinstance(value, type):
|
|
159
|
+
return any(
|
|
160
|
+
_contains_function(getattr(value, field.name))
|
|
161
|
+
for field in dataclasses.fields(value)
|
|
162
|
+
)
|
|
163
|
+
return callable(value) and not isinstance(value, type)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def equal(a: object, b: object) -> bool:
|
|
167
|
+
"""Check structural equality, like Elm's `==`.
|
|
168
|
+
|
|
169
|
+
Frozen dataclasses, tuples, strings, numbers, `Decimal`, `Dict` and `Set`
|
|
170
|
+
values all compare by value in Python, so this function uses `==`. `NaN` is
|
|
171
|
+
not equal to itself, as in Elm.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
a: The first value.
|
|
175
|
+
b: The second value.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
True when the two values are structurally equal.
|
|
179
|
+
|
|
180
|
+
Raises:
|
|
181
|
+
TypeError: If a value is a function, or holds a function anywhere
|
|
182
|
+
inside a tuple, a list or an SDK data type such as `Just`, `Ok` or
|
|
183
|
+
`Dict`. Elm's runtime also fails on these values.
|
|
184
|
+
"""
|
|
185
|
+
if _contains_function(a) or _contains_function(b):
|
|
186
|
+
raise TypeError("equal: cannot compare functions")
|
|
187
|
+
return bool(a == b)
|
morphir/sdk/aggregate.py
ADDED
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
"""Morphir.SDK.Aggregate: aggregations for large data sets.
|
|
2
|
+
|
|
3
|
+
An `Aggregation[A, K]` tells how to aggregate rows of type `A`: the key that
|
|
4
|
+
groups the rows, a filter that removes rows before the aggregation, and the
|
|
5
|
+
operator (count, sum, average, minimum, maximum or weighted average). Build one
|
|
6
|
+
with `count`, `sum_of`, `average_of`, `minimum_of`, `maximum_of` or
|
|
7
|
+
`weighted_average_of`, then set the key with `by_key` and the filter with
|
|
8
|
+
`with_filter`. All aggregated values are floats.
|
|
9
|
+
|
|
10
|
+
There are two ways to use an aggregation:
|
|
11
|
+
|
|
12
|
+
* `aggregate_map` to `aggregate_map4` map each row together with the aggregated
|
|
13
|
+
values of the group of that row.
|
|
14
|
+
* `group_by` and then `aggregate` yield one item for each group.
|
|
15
|
+
|
|
16
|
+
>>> from morphir.sdk import aggregate as Aggregate
|
|
17
|
+
>>> rows = (("a", 1.0), ("a", 2.0), ("b", 5.0))
|
|
18
|
+
>>> total = Aggregate.by_key(lambda r: r[0], Aggregate.sum_of(lambda r: r[1]))
|
|
19
|
+
>>> Aggregate.aggregate_map(total, lambda t, r: (r[0], r[1] / t), rows)
|
|
20
|
+
(('a', 0.3333333333333333), ('a', 0.6666666666666666), ('b', 1.0))
|
|
21
|
+
>>> Aggregate.aggregate(
|
|
22
|
+
... lambda key, inputs: (key, inputs(Aggregate.count())),
|
|
23
|
+
... Aggregate.group_by(lambda r: r[0], rows),
|
|
24
|
+
... )
|
|
25
|
+
(('a', 2.0), ('b', 1.0))
|
|
26
|
+
|
|
27
|
+
All functions take their arguments in Elm order, with the data last, and are
|
|
28
|
+
not curried. Lists are tuples: the functions accept any `Sequence` and yield a
|
|
29
|
+
tuple.
|
|
30
|
+
|
|
31
|
+
This module is the runtime API only. The Elm module also has
|
|
32
|
+
`constructAggregationCall`, `AggregationCall`, `AggregateValue` and
|
|
33
|
+
`ConstructAggregationError`. Those inspect Morphir IR values (`TypedValue`) for
|
|
34
|
+
the backends. They are tooling, they are not in the `Morphir.IR.SDK.Aggregate`
|
|
35
|
+
specification, and they are not part of this module.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
from dataclasses import dataclass
|
|
39
|
+
from typing import TYPE_CHECKING
|
|
40
|
+
|
|
41
|
+
from morphir.sdk import _compare, basics
|
|
42
|
+
from morphir.sdk import dict as dict_
|
|
43
|
+
from morphir.sdk.key import Key0, key0
|
|
44
|
+
|
|
45
|
+
if TYPE_CHECKING:
|
|
46
|
+
from collections.abc import Callable, Sequence
|
|
47
|
+
|
|
48
|
+
from morphir.sdk._compare import Comparable
|
|
49
|
+
from morphir.sdk.dict import Dict
|
|
50
|
+
|
|
51
|
+
__all__ = [
|
|
52
|
+
"Aggregation",
|
|
53
|
+
"Aggregator",
|
|
54
|
+
"Avg",
|
|
55
|
+
"Count",
|
|
56
|
+
"Max",
|
|
57
|
+
"Min",
|
|
58
|
+
"Operator",
|
|
59
|
+
"Sum",
|
|
60
|
+
"WAvg",
|
|
61
|
+
"aggregate",
|
|
62
|
+
"aggregate_map",
|
|
63
|
+
"aggregate_map2",
|
|
64
|
+
"aggregate_map3",
|
|
65
|
+
"aggregate_map4",
|
|
66
|
+
"average_of",
|
|
67
|
+
"by_key",
|
|
68
|
+
"count",
|
|
69
|
+
"group_by",
|
|
70
|
+
"maximum_of",
|
|
71
|
+
"minimum_of",
|
|
72
|
+
"sum_of",
|
|
73
|
+
"weighted_average_of",
|
|
74
|
+
"with_filter",
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# Operators
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass(frozen=True, slots=True)
|
|
82
|
+
class Count:
|
|
83
|
+
"""The operator that counts the rows."""
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass(frozen=True, slots=True)
|
|
87
|
+
class Sum[A]:
|
|
88
|
+
"""The operator that adds the values of the rows."""
|
|
89
|
+
|
|
90
|
+
get_value: Callable[[A], float]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass(frozen=True, slots=True)
|
|
94
|
+
class Avg[A]:
|
|
95
|
+
"""The operator that gives the average of the values of the rows."""
|
|
96
|
+
|
|
97
|
+
get_value: Callable[[A], float]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@dataclass(frozen=True, slots=True)
|
|
101
|
+
class Min[A]:
|
|
102
|
+
"""The operator that gives the smallest of the values of the rows."""
|
|
103
|
+
|
|
104
|
+
get_value: Callable[[A], float]
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@dataclass(frozen=True, slots=True)
|
|
108
|
+
class Max[A]:
|
|
109
|
+
"""The operator that gives the largest of the values of the rows."""
|
|
110
|
+
|
|
111
|
+
get_value: Callable[[A], float]
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@dataclass(frozen=True, slots=True)
|
|
115
|
+
class WAvg[A]:
|
|
116
|
+
"""The operator that gives the weighted average of the values of the rows."""
|
|
117
|
+
|
|
118
|
+
get_weight: Callable[[A], float]
|
|
119
|
+
get_value: Callable[[A], float]
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
type Operator[A] = Count | Sum[A] | Avg[A] | Min[A] | Max[A] | WAvg[A]
|
|
123
|
+
"""The aggregation operation to apply to the rows of a group."""
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@dataclass(frozen=True, slots=True)
|
|
127
|
+
class Aggregation[A, K]:
|
|
128
|
+
"""An aggregation on rows of type `A` with a key of type `K`.
|
|
129
|
+
|
|
130
|
+
Attributes:
|
|
131
|
+
key: A function that gives the key of a row. Rows with equal keys are
|
|
132
|
+
aggregated together.
|
|
133
|
+
filter: A test that a row must pass to be part of the aggregation.
|
|
134
|
+
operator: The aggregation operation to apply.
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
key: Callable[[A], K]
|
|
138
|
+
filter: Callable[[A], bool]
|
|
139
|
+
operator: Operator[A]
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
type Aggregator[A, K] = Callable[[Aggregation[A, K]], float]
|
|
143
|
+
"""A function that applies an aggregation to the rows of one group."""
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _always_true(_: object) -> bool:
|
|
147
|
+
return True
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _one(_: object) -> float:
|
|
151
|
+
return 1.0
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _operator_to_aggregation[A](operator: Operator[A]) -> Aggregation[A, Key0]:
|
|
155
|
+
return Aggregation(key=key0, filter=_always_true, operator=operator)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def count[A]() -> Aggregation[A, Key0]: # pyright: ignore[reportInvalidTypeVarUse]
|
|
159
|
+
"""Count the rows in a group.
|
|
160
|
+
|
|
161
|
+
In Elm `count` is a value. Here it is a function with no arguments, so that
|
|
162
|
+
the type checker can give each use its own row type.
|
|
163
|
+
"""
|
|
164
|
+
return _operator_to_aggregation(Count())
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def sum_of[A](get_value: Callable[[A], float]) -> Aggregation[A, Key0]:
|
|
168
|
+
"""Add the values that a function gives for each row of a group."""
|
|
169
|
+
return _operator_to_aggregation(Sum(get_value))
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def average_of[A](get_value: Callable[[A], float]) -> Aggregation[A, Key0]:
|
|
173
|
+
"""Give the average of the values that a function gives for each row."""
|
|
174
|
+
return _operator_to_aggregation(Avg(get_value))
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def minimum_of[A](get_value: Callable[[A], float]) -> Aggregation[A, Key0]:
|
|
178
|
+
"""Give the smallest of the values that a function gives for each row."""
|
|
179
|
+
return _operator_to_aggregation(Min(get_value))
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def maximum_of[A](get_value: Callable[[A], float]) -> Aggregation[A, Key0]:
|
|
183
|
+
"""Give the largest of the values that a function gives for each row."""
|
|
184
|
+
return _operator_to_aggregation(Max(get_value))
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def weighted_average_of[A](
|
|
188
|
+
get_weight: Callable[[A], float], get_value: Callable[[A], float]
|
|
189
|
+
) -> Aggregation[A, Key0]:
|
|
190
|
+
"""Give the weighted average of the values of the rows of a group.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
get_weight: A function that gives the weight of a row.
|
|
194
|
+
get_value: A function that gives the value of a row.
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
An aggregation that divides the sum of `weight * value` by the sum of
|
|
198
|
+
the weights. When the sum of the weights is zero the division follows
|
|
199
|
+
IEEE 754, as in Elm: the result is `NaN` or an infinity.
|
|
200
|
+
"""
|
|
201
|
+
return _operator_to_aggregation(WAvg(get_weight, get_value))
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def by_key[A, K](
|
|
205
|
+
key: Callable[[A], K], aggregation: Aggregation[A, Key0]
|
|
206
|
+
) -> Aggregation[A, K]:
|
|
207
|
+
"""Set the key of an aggregation. The filter and the operator stay."""
|
|
208
|
+
return Aggregation(
|
|
209
|
+
key=key, filter=aggregation.filter, operator=aggregation.operator
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def with_filter[A, K](
|
|
214
|
+
filter: Callable[[A], bool],
|
|
215
|
+
aggregation: Aggregation[A, K],
|
|
216
|
+
) -> Aggregation[A, K]:
|
|
217
|
+
"""Set the filter of an aggregation. The key and the operator stay.
|
|
218
|
+
|
|
219
|
+
A filter that the aggregation already has is replaced, as in Elm.
|
|
220
|
+
"""
|
|
221
|
+
return Aggregation(
|
|
222
|
+
key=aggregation.key, filter=filter, operator=aggregation.operator
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
# Aggregation of rows
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _elm_min(a: float, b: float) -> float:
|
|
230
|
+
return a if a < b else b
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _elm_max(a: float, b: float) -> float:
|
|
234
|
+
return a if a > b else b
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _fold[A](
|
|
238
|
+
get_key: Callable[[A], object],
|
|
239
|
+
get_value: Callable[[A], float],
|
|
240
|
+
combine: Callable[[float, float], float],
|
|
241
|
+
rows: Sequence[A],
|
|
242
|
+
) -> dict[object, float]:
|
|
243
|
+
totals: dict[object, float] = {}
|
|
244
|
+
for row in rows:
|
|
245
|
+
key = _compare.freeze(get_key(row))
|
|
246
|
+
value = get_value(row)
|
|
247
|
+
totals[key] = combine(totals[key], value) if key in totals else value
|
|
248
|
+
return totals
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _add(a: float, b: float) -> float:
|
|
252
|
+
return a + b
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _ratio(
|
|
256
|
+
numerators: dict[object, float], denominators: dict[object, float]
|
|
257
|
+
) -> dict[object, float]:
|
|
258
|
+
return {
|
|
259
|
+
key: basics.divide(numerator, denominators.get(key, 0.0))
|
|
260
|
+
for key, numerator in numerators.items()
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _aggregate_help[A](
|
|
265
|
+
get_key: Callable[[A], object], operator: Operator[A], rows: Sequence[A]
|
|
266
|
+
) -> dict[object, float]:
|
|
267
|
+
match operator:
|
|
268
|
+
case Count():
|
|
269
|
+
return _fold(get_key, _one, _add, rows)
|
|
270
|
+
case Sum(get_value):
|
|
271
|
+
return _fold(get_key, get_value, _add, rows)
|
|
272
|
+
case Avg(get_value):
|
|
273
|
+
return _ratio(
|
|
274
|
+
_fold(get_key, get_value, _add, rows),
|
|
275
|
+
_fold(get_key, _one, _add, rows),
|
|
276
|
+
)
|
|
277
|
+
case Min(get_value):
|
|
278
|
+
return _fold(get_key, get_value, _elm_min, rows)
|
|
279
|
+
case Max(get_value):
|
|
280
|
+
return _fold(get_key, get_value, _elm_max, rows)
|
|
281
|
+
case WAvg(get_weight, get_value):
|
|
282
|
+
|
|
283
|
+
def weighted(row: A) -> float:
|
|
284
|
+
return get_weight(row) * get_value(row)
|
|
285
|
+
|
|
286
|
+
return _ratio(
|
|
287
|
+
_fold(get_key, weighted, _add, rows),
|
|
288
|
+
_fold(get_key, get_weight, _add, rows),
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _lookup[A, K](
|
|
293
|
+
aggregation: Aggregation[A, K], rows: Sequence[A]
|
|
294
|
+
) -> Callable[[A], float]:
|
|
295
|
+
included = tuple(row for row in rows if aggregation.filter(row))
|
|
296
|
+
totals = _aggregate_help(aggregation.key, aggregation.operator, included)
|
|
297
|
+
|
|
298
|
+
def value_for(row: A) -> float:
|
|
299
|
+
return totals.get(_compare.freeze(aggregation.key(row)), 0.0)
|
|
300
|
+
|
|
301
|
+
return value_for
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def aggregate_map[A, B, K1](
|
|
305
|
+
agg1: Aggregation[A, K1], f: Callable[[float, A], B], xs: Sequence[A]
|
|
306
|
+
) -> tuple[B, ...]:
|
|
307
|
+
"""Map each row together with one aggregated value.
|
|
308
|
+
|
|
309
|
+
The aggregation is computed one time on the rows that pass its filter. Each
|
|
310
|
+
row of the list, also one that does not pass the filter, then gets the
|
|
311
|
+
aggregated value for its key.
|
|
312
|
+
|
|
313
|
+
Args:
|
|
314
|
+
agg1: The aggregation.
|
|
315
|
+
f: The mapping function. It gets the aggregated value for the key of
|
|
316
|
+
the row, then the row. The value is 0 when no row with that key
|
|
317
|
+
passed the filter.
|
|
318
|
+
xs: The rows.
|
|
319
|
+
|
|
320
|
+
Returns:
|
|
321
|
+
The mapped rows, in the order of the input.
|
|
322
|
+
"""
|
|
323
|
+
value1 = _lookup(agg1, xs)
|
|
324
|
+
return tuple(f(value1(x), x) for x in xs)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def aggregate_map2[A, B, K1, K2](
|
|
328
|
+
agg1: Aggregation[A, K1],
|
|
329
|
+
agg2: Aggregation[A, K2],
|
|
330
|
+
f: Callable[[float, float, A], B],
|
|
331
|
+
xs: Sequence[A],
|
|
332
|
+
) -> tuple[B, ...]:
|
|
333
|
+
"""Map each row together with two aggregated values. See `aggregate_map`."""
|
|
334
|
+
value1 = _lookup(agg1, xs)
|
|
335
|
+
value2 = _lookup(agg2, xs)
|
|
336
|
+
return tuple(f(value1(x), value2(x), x) for x in xs)
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def aggregate_map3[A, B, K1, K2, K3](
|
|
340
|
+
agg1: Aggregation[A, K1],
|
|
341
|
+
agg2: Aggregation[A, K2],
|
|
342
|
+
agg3: Aggregation[A, K3],
|
|
343
|
+
f: Callable[[float, float, float, A], B],
|
|
344
|
+
xs: Sequence[A],
|
|
345
|
+
) -> tuple[B, ...]:
|
|
346
|
+
"""Map each row together with three aggregated values. See `aggregate_map`."""
|
|
347
|
+
value1 = _lookup(agg1, xs)
|
|
348
|
+
value2 = _lookup(agg2, xs)
|
|
349
|
+
value3 = _lookup(agg3, xs)
|
|
350
|
+
return tuple(f(value1(x), value2(x), value3(x), x) for x in xs)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def aggregate_map4[A, B, K1, K2, K3, K4](
|
|
354
|
+
agg1: Aggregation[A, K1],
|
|
355
|
+
agg2: Aggregation[A, K2],
|
|
356
|
+
agg3: Aggregation[A, K3],
|
|
357
|
+
agg4: Aggregation[A, K4],
|
|
358
|
+
f: Callable[[float, float, float, float, A], B],
|
|
359
|
+
xs: Sequence[A],
|
|
360
|
+
) -> tuple[B, ...]:
|
|
361
|
+
"""Map each row together with four aggregated values. See `aggregate_map`."""
|
|
362
|
+
value1 = _lookup(agg1, xs)
|
|
363
|
+
value2 = _lookup(agg2, xs)
|
|
364
|
+
value3 = _lookup(agg3, xs)
|
|
365
|
+
value4 = _lookup(agg4, xs)
|
|
366
|
+
return tuple(f(value1(x), value2(x), value3(x), value4(x), x) for x in xs)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
# Grouping
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def group_by[A, K: Comparable](
|
|
373
|
+
get_key: Callable[[A], K], xs: Sequence[A]
|
|
374
|
+
) -> Dict[K, tuple[A, ...]]:
|
|
375
|
+
"""Group the rows of a list into a dictionary.
|
|
376
|
+
|
|
377
|
+
Args:
|
|
378
|
+
get_key: A function that gives the key of a row.
|
|
379
|
+
xs: The rows.
|
|
380
|
+
|
|
381
|
+
Returns:
|
|
382
|
+
A dictionary from each key to the rows with that key. The rows of a
|
|
383
|
+
group keep the order that they have in the list.
|
|
384
|
+
|
|
385
|
+
Raises:
|
|
386
|
+
TypeError: If the keys are not comparable.
|
|
387
|
+
"""
|
|
388
|
+
groups: dict[K, list[A]] = {}
|
|
389
|
+
for x in xs:
|
|
390
|
+
groups.setdefault(_compare.freeze(get_key(x)), []).append(x)
|
|
391
|
+
return dict_.from_list([(key, tuple(rows)) for key, rows in groups.items()])
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def aggregate[A, B, K: Comparable](
|
|
395
|
+
f: Callable[[K, Aggregator[A, Key0]], B], groups: Dict[K, tuple[A, ...]]
|
|
396
|
+
) -> tuple[B, ...]:
|
|
397
|
+
"""Aggregate a dictionary of groups into a list with one item for each key.
|
|
398
|
+
|
|
399
|
+
This function is for use after `group_by`.
|
|
400
|
+
|
|
401
|
+
Args:
|
|
402
|
+
f: A function that gets a key and an aggregator, and yields the item for
|
|
403
|
+
that key. The aggregator is a function that takes an aggregation
|
|
404
|
+
with no key (`count()`, `sum_of(...)`, also with `with_filter`) and
|
|
405
|
+
yields the aggregated value for the rows of the group. The value is
|
|
406
|
+
0 when no row passes the filter.
|
|
407
|
+
groups: The dictionary of groups.
|
|
408
|
+
|
|
409
|
+
Returns:
|
|
410
|
+
One item for each key, in key order.
|
|
411
|
+
"""
|
|
412
|
+
|
|
413
|
+
def item(key: K, rows: tuple[A, ...]) -> B:
|
|
414
|
+
def aggregator(aggregation: Aggregation[A, Key0]) -> float:
|
|
415
|
+
included = tuple(row for row in rows if aggregation.filter(row))
|
|
416
|
+
totals = _aggregate_help(aggregation.key, aggregation.operator, included)
|
|
417
|
+
return totals.get(0, 0.0)
|
|
418
|
+
|
|
419
|
+
return f(key, aggregator)
|
|
420
|
+
|
|
421
|
+
return tuple(item(key, rows) for key, rows in dict_.to_list(groups))
|