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/decimal.py
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
"""Morphir.SDK.Decimal: arbitrary-precision decimal numbers.
|
|
2
|
+
|
|
3
|
+
A `Decimal` is the standard library `decimal.Decimal`. All arithmetic goes
|
|
4
|
+
through a context that is local to this module, with 50 significant digits and
|
|
5
|
+
half-up rounding. The module never reads or changes the global decimal context,
|
|
6
|
+
so other users of the `decimal` module see no effect, and a change to the global
|
|
7
|
+
context has no effect on the results here.
|
|
8
|
+
|
|
9
|
+
`Decimal` values compare by numeric value: `1.50 == 1.5`, and both have the same
|
|
10
|
+
hash.
|
|
11
|
+
|
|
12
|
+
Some names in this module shadow Python built-ins (`abs`, `round`). Import the
|
|
13
|
+
module with an alias, not the names:
|
|
14
|
+
|
|
15
|
+
>>> from morphir.sdk import decimal as Decimal
|
|
16
|
+
>>> Decimal.to_string(Decimal.add(Decimal.from_float(0.1), Decimal.bps(5)))
|
|
17
|
+
'0.1005'
|
|
18
|
+
|
|
19
|
+
Departures from the Elm runtime:
|
|
20
|
+
|
|
21
|
+
* `from_float` raises `ValueError` for `NaN` and the infinities, which a
|
|
22
|
+
`Decimal` in this module cannot hold.
|
|
23
|
+
* `shift_decimal_left` moves the exponent and uses no float. The Elm runtime
|
|
24
|
+
multiplies by the float `10 ^ -n`.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
import decimal as _decimal
|
|
28
|
+
import math
|
|
29
|
+
import re
|
|
30
|
+
|
|
31
|
+
from morphir.sdk import maybe
|
|
32
|
+
from morphir.sdk._compare import Order
|
|
33
|
+
from morphir.sdk.maybe import Just, Maybe, Nothing
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"Decimal",
|
|
37
|
+
"abs",
|
|
38
|
+
"add",
|
|
39
|
+
"bps",
|
|
40
|
+
"compare",
|
|
41
|
+
"div",
|
|
42
|
+
"div_with_default",
|
|
43
|
+
"eq",
|
|
44
|
+
"from_float",
|
|
45
|
+
"from_int",
|
|
46
|
+
"from_string",
|
|
47
|
+
"gt",
|
|
48
|
+
"gte",
|
|
49
|
+
"hundred",
|
|
50
|
+
"hundredth",
|
|
51
|
+
"lt",
|
|
52
|
+
"lte",
|
|
53
|
+
"million",
|
|
54
|
+
"millionth",
|
|
55
|
+
"minus_one",
|
|
56
|
+
"mul",
|
|
57
|
+
"negate",
|
|
58
|
+
"neq",
|
|
59
|
+
"one",
|
|
60
|
+
"round",
|
|
61
|
+
"shift_decimal_left",
|
|
62
|
+
"shift_decimal_right",
|
|
63
|
+
"sub",
|
|
64
|
+
"tenth",
|
|
65
|
+
"thousand",
|
|
66
|
+
"thousandth",
|
|
67
|
+
"to_float",
|
|
68
|
+
"to_string",
|
|
69
|
+
"truncate",
|
|
70
|
+
"zero",
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
Decimal = _decimal.Decimal
|
|
74
|
+
"""The standard library decimal type."""
|
|
75
|
+
|
|
76
|
+
PRECISION = 50
|
|
77
|
+
"""The number of significant digits that arithmetic results keep."""
|
|
78
|
+
|
|
79
|
+
# The exponent limits are as wide as the library allows, so that overflow is not
|
|
80
|
+
# a practical concern.
|
|
81
|
+
_CONTEXT = _decimal.Context(
|
|
82
|
+
prec=PRECISION,
|
|
83
|
+
rounding=_decimal.ROUND_HALF_UP,
|
|
84
|
+
Emin=_decimal.MIN_EMIN,
|
|
85
|
+
Emax=_decimal.MAX_EMAX,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# Elm's grammar: [<sign>]<digits>[.<digits>][e[<sign>]<digits>]. The standard
|
|
89
|
+
# library also accepts "Infinity", "NaN", underscores, white space and digits
|
|
90
|
+
# that are not ASCII; Elm parses none of these.
|
|
91
|
+
_ELM_DECIMAL = re.compile(r"[+-]?[0-9]+(\.[0-9]+)?([eE][+-]?[0-9]+)?")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# Constants
|
|
95
|
+
|
|
96
|
+
zero: Decimal = Decimal(0)
|
|
97
|
+
"""The number 0."""
|
|
98
|
+
|
|
99
|
+
one: Decimal = Decimal(1)
|
|
100
|
+
"""The number 1."""
|
|
101
|
+
|
|
102
|
+
minus_one: Decimal = Decimal(-1)
|
|
103
|
+
"""The number -1."""
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# Convert from
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def from_int(n: int) -> Decimal:
|
|
110
|
+
"""Convert an integer to a `Decimal`. The conversion is exact."""
|
|
111
|
+
return Decimal(n)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def from_float(f: float) -> Decimal:
|
|
115
|
+
"""Convert a float to a `Decimal`.
|
|
116
|
+
|
|
117
|
+
The result has the shortest digits that identify the float, so
|
|
118
|
+
`from_float(0.1)` is exactly 0.1. Negative zero becomes zero.
|
|
119
|
+
|
|
120
|
+
Raises:
|
|
121
|
+
ValueError: If the float is `NaN` or an infinity.
|
|
122
|
+
"""
|
|
123
|
+
if not math.isfinite(f):
|
|
124
|
+
raise ValueError(f"from_float: {f!r} is not a finite number")
|
|
125
|
+
return zero if f == 0 else Decimal(repr(f))
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def from_string(s: str) -> Maybe[Decimal]:
|
|
129
|
+
"""Parse a `Decimal`.
|
|
130
|
+
|
|
131
|
+
The accepted form is an optional sign, digits, an optional fraction and an
|
|
132
|
+
optional exponent: `[+-]?digits(.digits)?([eE][+-]?digits)?`. Anything else
|
|
133
|
+
yields `Nothing`. The conversion is exact; it keeps all the digits.
|
|
134
|
+
"""
|
|
135
|
+
if not _ELM_DECIMAL.fullmatch(s):
|
|
136
|
+
return Nothing()
|
|
137
|
+
try:
|
|
138
|
+
parsed = Decimal(s)
|
|
139
|
+
except _decimal.InvalidOperation:
|
|
140
|
+
# The exponent is outside the range that the library can hold.
|
|
141
|
+
return Nothing()
|
|
142
|
+
return Just(parsed) if parsed.is_finite() else Nothing()
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# Convert from a known exponent
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def hundred(n: int) -> Decimal:
|
|
149
|
+
"""Return `n` hundreds."""
|
|
150
|
+
return Decimal(n).scaleb(2, _CONTEXT)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def thousand(n: int) -> Decimal:
|
|
154
|
+
"""Return `n` thousands."""
|
|
155
|
+
return Decimal(n).scaleb(3, _CONTEXT)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def million(n: int) -> Decimal:
|
|
159
|
+
"""Return `n` millions."""
|
|
160
|
+
return Decimal(n).scaleb(6, _CONTEXT)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def tenth(n: int) -> Decimal:
|
|
164
|
+
"""Return `n` tenths, with no float arithmetic."""
|
|
165
|
+
return Decimal(n).scaleb(-1, _CONTEXT)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def hundredth(n: int) -> Decimal:
|
|
169
|
+
"""Return `n` hundredths, with no float arithmetic."""
|
|
170
|
+
return Decimal(n).scaleb(-2, _CONTEXT)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def thousandth(n: int) -> Decimal:
|
|
174
|
+
"""Return `n` thousandths, with no float arithmetic."""
|
|
175
|
+
return Decimal(n).scaleb(-3, _CONTEXT)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def bps(n: int) -> Decimal:
|
|
179
|
+
"""Return `n` basis points (ten-thousandths), with no float arithmetic."""
|
|
180
|
+
return Decimal(n).scaleb(-4, _CONTEXT)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def millionth(n: int) -> Decimal:
|
|
184
|
+
"""Return `n` millionths, with no float arithmetic."""
|
|
185
|
+
return Decimal(n).scaleb(-6, _CONTEXT)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
# Convert to
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def to_string(d: Decimal) -> str:
|
|
192
|
+
"""Convert a `Decimal` to a string.
|
|
193
|
+
|
|
194
|
+
The result is always in positional notation, never in exponent notation,
|
|
195
|
+
and has no trailing zeros in the fraction.
|
|
196
|
+
"""
|
|
197
|
+
text = format(d, "f")
|
|
198
|
+
if "." in text:
|
|
199
|
+
text = text.rstrip("0").rstrip(".")
|
|
200
|
+
return "0" if text in ("-0", "") else text
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def to_float(d: Decimal) -> float:
|
|
204
|
+
"""Convert a `Decimal` to the nearest float. This can lose precision."""
|
|
205
|
+
return float(d)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
# Arithmetic
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def add(a: Decimal, b: Decimal) -> Decimal:
|
|
212
|
+
"""Add two decimals."""
|
|
213
|
+
return _CONTEXT.add(a, b)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def sub(a: Decimal, b: Decimal) -> Decimal:
|
|
217
|
+
"""Subtract the second decimal from the first."""
|
|
218
|
+
return _CONTEXT.subtract(a, b)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def mul(a: Decimal, b: Decimal) -> Decimal:
|
|
222
|
+
"""Multiply two decimals."""
|
|
223
|
+
return _CONTEXT.multiply(a, b)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def div(a: Decimal, b: Decimal) -> Maybe[Decimal]:
|
|
227
|
+
"""Divide two decimals, or yield `Nothing` when the divisor is zero.
|
|
228
|
+
|
|
229
|
+
A quotient that is not exact has 50 significant digits.
|
|
230
|
+
"""
|
|
231
|
+
return Nothing() if b.is_zero() else Just(_CONTEXT.divide(a, b))
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def div_with_default(default: Decimal, a: Decimal, b: Decimal) -> Decimal:
|
|
235
|
+
"""Divide two decimals, or yield the default when the divisor is zero."""
|
|
236
|
+
return maybe.with_default(default, div(a, b))
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def shift_decimal_left(n: int, d: Decimal) -> Decimal:
|
|
240
|
+
"""Move the decimal point `n` digits to the left, which divides by 10^n."""
|
|
241
|
+
return d.scaleb(-n, _CONTEXT)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def shift_decimal_right(n: int, d: Decimal) -> Decimal:
|
|
245
|
+
"""Move the decimal point `n` digits to the right, which multiplies by 10^n."""
|
|
246
|
+
return d.scaleb(n, _CONTEXT)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def negate(d: Decimal) -> Decimal:
|
|
250
|
+
"""Change the sign of a decimal. Zero stays zero."""
|
|
251
|
+
return zero if d.is_zero() else d.copy_negate()
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def abs(d: Decimal) -> Decimal:
|
|
255
|
+
"""Return the absolute value of a decimal."""
|
|
256
|
+
return d.copy_abs()
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
# Rounding
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def truncate(d: Decimal) -> Decimal:
|
|
263
|
+
"""Round toward zero to an integer."""
|
|
264
|
+
return d.to_integral_value(rounding=_decimal.ROUND_DOWN, context=_CONTEXT)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def round(d: Decimal) -> Decimal:
|
|
268
|
+
"""Round to the nearest integer. A half goes away from zero."""
|
|
269
|
+
return d.to_integral_value(rounding=_decimal.ROUND_HALF_UP, context=_CONTEXT)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
# Compare
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def compare(a: Decimal, b: Decimal) -> Order:
|
|
276
|
+
"""Compare two decimals by numeric value."""
|
|
277
|
+
if a < b:
|
|
278
|
+
return Order.LT
|
|
279
|
+
if a > b:
|
|
280
|
+
return Order.GT
|
|
281
|
+
return Order.EQ
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def eq(a: Decimal, b: Decimal) -> bool:
|
|
285
|
+
"""Check `a == b` by numeric value, so 1.50 equals 1.5."""
|
|
286
|
+
return a == b
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def neq(a: Decimal, b: Decimal) -> bool:
|
|
290
|
+
"""Check `a /= b` by numeric value."""
|
|
291
|
+
return a != b
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def gt(a: Decimal, b: Decimal) -> bool:
|
|
295
|
+
"""Check `a > b`."""
|
|
296
|
+
return a > b
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def gte(a: Decimal, b: Decimal) -> bool:
|
|
300
|
+
"""Check `a >= b`."""
|
|
301
|
+
return a >= b
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def lt(a: Decimal, b: Decimal) -> bool:
|
|
305
|
+
"""Check `a < b`."""
|
|
306
|
+
return a < b
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def lte(a: Decimal, b: Decimal) -> bool:
|
|
310
|
+
"""Check `a <= b`."""
|
|
311
|
+
return a <= b
|
morphir/sdk/dict.py
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
"""Morphir.SDK.Dict: a mapping from comparable keys to values.
|
|
2
|
+
|
|
3
|
+
This module follows elm/core 1.0.5. A `Dict[K, V]` is a frozen dataclass that
|
|
4
|
+
holds its entries as a tuple of `(key, value)` pairs, sorted in ascending order
|
|
5
|
+
by `basics.compare` on the key, with no key twice. Thus:
|
|
6
|
+
|
|
7
|
+
* `to_list`, `keys` and `values` yield the entries in key order, as in Elm.
|
|
8
|
+
* Two dicts with the same entries are equal (`==`) and have the same hash, no
|
|
9
|
+
matter in which order the entries were inserted.
|
|
10
|
+
* Keys can be any comparable value: numbers, strings, and tuples or lists of
|
|
11
|
+
comparables. A key that is not comparable raises `TypeError`.
|
|
12
|
+
|
|
13
|
+
Build a `Dict` with `empty`, `singleton`, `from_list` and `insert`. Do not pass
|
|
14
|
+
entries to the `Dict` constructor, because it does not sort them.
|
|
15
|
+
|
|
16
|
+
All functions take their arguments in Elm order, with the dict last, and yield
|
|
17
|
+
a new `Dict`; nothing is mutated. Lookup is a binary search, O(log n). `insert`
|
|
18
|
+
and `remove` copy the entries, O(n).
|
|
19
|
+
|
|
20
|
+
Some names in this module shadow Python built-ins (`filter`, `map`). Import the
|
|
21
|
+
module with an alias, not the names:
|
|
22
|
+
|
|
23
|
+
>>> from morphir.sdk import dict as Dict
|
|
24
|
+
>>> Dict.get("a", Dict.from_list((("b", 2), ("a", 1))))
|
|
25
|
+
Just(value=1)
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from dataclasses import dataclass
|
|
29
|
+
from typing import TYPE_CHECKING
|
|
30
|
+
|
|
31
|
+
from morphir.sdk import _compare
|
|
32
|
+
from morphir.sdk._compare import Comparable, Order
|
|
33
|
+
from morphir.sdk.maybe import Just, Maybe, Nothing
|
|
34
|
+
|
|
35
|
+
if TYPE_CHECKING:
|
|
36
|
+
from collections.abc import Callable, Iterator, Sequence
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
"Dict",
|
|
40
|
+
"diff",
|
|
41
|
+
"empty",
|
|
42
|
+
"filter",
|
|
43
|
+
"foldl",
|
|
44
|
+
"foldr",
|
|
45
|
+
"from_list",
|
|
46
|
+
"get",
|
|
47
|
+
"insert",
|
|
48
|
+
"intersect",
|
|
49
|
+
"is_empty",
|
|
50
|
+
"keys",
|
|
51
|
+
"map",
|
|
52
|
+
"member",
|
|
53
|
+
"merge",
|
|
54
|
+
"partition",
|
|
55
|
+
"remove",
|
|
56
|
+
"singleton",
|
|
57
|
+
"size",
|
|
58
|
+
"to_list",
|
|
59
|
+
"union",
|
|
60
|
+
"update",
|
|
61
|
+
"values",
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass(frozen=True, slots=True)
|
|
66
|
+
class Dict[K: Comparable, V]:
|
|
67
|
+
"""An immutable dictionary with its entries sorted by key.
|
|
68
|
+
|
|
69
|
+
Use `from_list`, `singleton` or `insert` to build one. The constructor
|
|
70
|
+
accepts only entries that are already sorted by key with no key twice, and
|
|
71
|
+
stores every list inside a key as a tuple.
|
|
72
|
+
|
|
73
|
+
Raises:
|
|
74
|
+
ValueError: If the entries are not sorted by key, or a key repeats.
|
|
75
|
+
TypeError: If two keys are not comparable with each other.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
entries: tuple[tuple[K, V], ...] = ()
|
|
79
|
+
|
|
80
|
+
def __post_init__(self) -> None:
|
|
81
|
+
entries = tuple((_compare.freeze(key), value) for key, value in self.entries)
|
|
82
|
+
_compare.check_strictly_ascending([key for key, _ in entries], "Dict")
|
|
83
|
+
object.__setattr__(self, "entries", entries)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _key_of[K: Comparable, V](entry: tuple[K, V]) -> K:
|
|
87
|
+
return entry[0]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _search[K: Comparable, V](key: K, dict_: Dict[K, V]) -> tuple[bool, int]:
|
|
91
|
+
return _compare.search(key, dict_.entries, _key_of)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# Build
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def empty[K: Comparable, V]() -> Dict[K, V]: # pyright: ignore[reportInvalidTypeVarUse]
|
|
98
|
+
"""Create an empty dictionary."""
|
|
99
|
+
return Dict()
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def singleton[K: Comparable, V](key: K, value: V) -> Dict[K, V]:
|
|
103
|
+
"""Create a dictionary with one entry."""
|
|
104
|
+
return Dict(((key, value),))
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def insert[K: Comparable, V](key: K, value: V, dict_: Dict[K, V]) -> Dict[K, V]:
|
|
108
|
+
"""Insert an entry. A value that is already there for the key is replaced.
|
|
109
|
+
|
|
110
|
+
Raises:
|
|
111
|
+
TypeError: If the key is not comparable with the keys in the dictionary.
|
|
112
|
+
"""
|
|
113
|
+
found, index = _search(key, dict_)
|
|
114
|
+
entries = dict_.entries
|
|
115
|
+
after = entries[index + 1 :] if found else entries[index:]
|
|
116
|
+
return Dict((*entries[:index], (key, value), *after))
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def update[K: Comparable, V](
|
|
120
|
+
key: K, alter: Callable[[Maybe[V]], Maybe[V]], dict_: Dict[K, V]
|
|
121
|
+
) -> Dict[K, V]:
|
|
122
|
+
"""Change the value for a key.
|
|
123
|
+
|
|
124
|
+
The function gets `Just` the current value, or `Nothing` when the key is not
|
|
125
|
+
there. When it yields `Nothing`, the key is removed.
|
|
126
|
+
"""
|
|
127
|
+
match alter(get(key, dict_)):
|
|
128
|
+
case Just(value):
|
|
129
|
+
return insert(key, value, dict_)
|
|
130
|
+
case Nothing():
|
|
131
|
+
return remove(key, dict_)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def remove[K: Comparable, V](key: K, dict_: Dict[K, V]) -> Dict[K, V]:
|
|
135
|
+
"""Remove the entry for a key. A key that is not there changes nothing."""
|
|
136
|
+
found, index = _search(key, dict_)
|
|
137
|
+
if not found:
|
|
138
|
+
return dict_
|
|
139
|
+
return Dict((*dict_.entries[:index], *dict_.entries[index + 1 :]))
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# Query
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def is_empty[K: Comparable, V](dict_: Dict[K, V]) -> bool:
|
|
146
|
+
"""Check that a dictionary is empty."""
|
|
147
|
+
return not dict_.entries
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def member[K: Comparable, V](key: K, dict_: Dict[K, V]) -> bool:
|
|
151
|
+
"""Check that a key is in a dictionary."""
|
|
152
|
+
return _search(key, dict_)[0]
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def get[K: Comparable, V](key: K, dict_: Dict[K, V]) -> Maybe[V]:
|
|
156
|
+
"""Return the value for a key, or `Nothing` when the key is not there."""
|
|
157
|
+
found, index = _search(key, dict_)
|
|
158
|
+
return Just(dict_.entries[index][1]) if found else Nothing()
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def size[K: Comparable, V](dict_: Dict[K, V]) -> int:
|
|
162
|
+
"""Return the number of entries in a dictionary."""
|
|
163
|
+
return len(dict_.entries)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
# Lists
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def keys[K: Comparable, V](dict_: Dict[K, V]) -> tuple[K, ...]:
|
|
170
|
+
"""Return the keys, from lowest to highest."""
|
|
171
|
+
return tuple(key for key, _ in dict_.entries)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def values[K: Comparable, V](dict_: Dict[K, V]) -> tuple[V, ...]:
|
|
175
|
+
"""Return the values, in the order of their keys."""
|
|
176
|
+
return tuple(value for _, value in dict_.entries)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def to_list[K: Comparable, V](dict_: Dict[K, V]) -> tuple[tuple[K, V], ...]:
|
|
180
|
+
"""Return the `(key, value)` pairs, sorted by key."""
|
|
181
|
+
return dict_.entries
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def from_list[K: Comparable, V](pairs: Sequence[tuple[K, V]]) -> Dict[K, V]:
|
|
185
|
+
"""Create a dictionary from `(key, value)` pairs.
|
|
186
|
+
|
|
187
|
+
When a key occurs more than once, the last pair wins, as in Elm.
|
|
188
|
+
|
|
189
|
+
Raises:
|
|
190
|
+
TypeError: If the keys are not comparable with each other.
|
|
191
|
+
"""
|
|
192
|
+
key_of = _compare.sort_key(_compare.compare)
|
|
193
|
+
entries: list[tuple[K, V]] = []
|
|
194
|
+
# The sort is stable, so the last pair for a key is the last one in its run.
|
|
195
|
+
for pair in sorted(pairs, key=lambda pair: key_of(pair[0])):
|
|
196
|
+
if entries and _compare.compare(entries[-1][0], pair[0]) is Order.EQ:
|
|
197
|
+
entries[-1] = pair
|
|
198
|
+
else:
|
|
199
|
+
entries.append(pair)
|
|
200
|
+
return Dict(tuple(entries))
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
# Transform
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def map[K: Comparable, A, B](f: Callable[[K, A], B], dict_: Dict[K, A]) -> Dict[K, B]:
|
|
207
|
+
"""Apply a function to every value. The function gets the key first."""
|
|
208
|
+
return Dict(tuple((key, f(key, value)) for key, value in dict_.entries))
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def foldl[K: Comparable, V, R](
|
|
212
|
+
f: Callable[[K, V, R], R], initial: R, dict_: Dict[K, V]
|
|
213
|
+
) -> R:
|
|
214
|
+
"""Reduce the entries from the lowest key to the highest."""
|
|
215
|
+
accumulator = initial
|
|
216
|
+
for key, value in dict_.entries:
|
|
217
|
+
accumulator = f(key, value, accumulator)
|
|
218
|
+
return accumulator
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def foldr[K: Comparable, V, R](
|
|
222
|
+
f: Callable[[K, V, R], R], initial: R, dict_: Dict[K, V]
|
|
223
|
+
) -> R:
|
|
224
|
+
"""Reduce the entries from the highest key to the lowest."""
|
|
225
|
+
accumulator = initial
|
|
226
|
+
for key, value in reversed(dict_.entries):
|
|
227
|
+
accumulator = f(key, value, accumulator)
|
|
228
|
+
return accumulator
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def filter[K: Comparable, V](
|
|
232
|
+
predicate: Callable[[K, V], bool], dict_: Dict[K, V]
|
|
233
|
+
) -> Dict[K, V]:
|
|
234
|
+
"""Keep the entries that pass the test."""
|
|
235
|
+
return Dict(tuple(entry for entry in dict_.entries if predicate(*entry)))
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def partition[K: Comparable, V](
|
|
239
|
+
predicate: Callable[[K, V], bool], dict_: Dict[K, V]
|
|
240
|
+
) -> tuple[Dict[K, V], Dict[K, V]]:
|
|
241
|
+
"""Split a dictionary into the entries that pass the test and those that fail."""
|
|
242
|
+
passed: list[tuple[K, V]] = []
|
|
243
|
+
failed: list[tuple[K, V]] = []
|
|
244
|
+
for entry in dict_.entries:
|
|
245
|
+
(passed if predicate(*entry) else failed).append(entry)
|
|
246
|
+
return (Dict(tuple(passed)), Dict(tuple(failed)))
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
# Combine
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _walk[K: Comparable, A, B](
|
|
253
|
+
left: Dict[K, A], right: Dict[K, B]
|
|
254
|
+
) -> Iterator[tuple[K, Maybe[A], Maybe[B]]]:
|
|
255
|
+
"""Walk two dictionaries in one pass, in ascending key order.
|
|
256
|
+
|
|
257
|
+
Each step yields the key with `Just` the value from each side that has it.
|
|
258
|
+
"""
|
|
259
|
+
lefts = left.entries
|
|
260
|
+
rights = right.entries
|
|
261
|
+
i = 0
|
|
262
|
+
j = 0
|
|
263
|
+
while i < len(lefts) and j < len(rights):
|
|
264
|
+
order = _compare.compare(lefts[i][0], rights[j][0])
|
|
265
|
+
if order is Order.LT:
|
|
266
|
+
yield (lefts[i][0], Just(lefts[i][1]), Nothing())
|
|
267
|
+
i += 1
|
|
268
|
+
elif order is Order.GT:
|
|
269
|
+
yield (rights[j][0], Nothing(), Just(rights[j][1]))
|
|
270
|
+
j += 1
|
|
271
|
+
else:
|
|
272
|
+
yield (lefts[i][0], Just(lefts[i][1]), Just(rights[j][1]))
|
|
273
|
+
i += 1
|
|
274
|
+
j += 1
|
|
275
|
+
for key, a in lefts[i:]:
|
|
276
|
+
yield (key, Just(a), Nothing())
|
|
277
|
+
for key, b in rights[j:]:
|
|
278
|
+
yield (key, Nothing(), Just(b))
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def union[K: Comparable, V](left: Dict[K, V], right: Dict[K, V]) -> Dict[K, V]:
|
|
282
|
+
"""Combine two dictionaries. For a key in both, the first dictionary wins."""
|
|
283
|
+
entries: list[tuple[K, V]] = []
|
|
284
|
+
for key, a, b in _walk(left, right):
|
|
285
|
+
if isinstance(a, Just):
|
|
286
|
+
entries.append((key, a.value))
|
|
287
|
+
elif isinstance(b, Just):
|
|
288
|
+
entries.append((key, b.value))
|
|
289
|
+
return Dict(tuple(entries))
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def intersect[K: Comparable, V, W](left: Dict[K, V], right: Dict[K, W]) -> Dict[K, V]:
|
|
293
|
+
"""Keep the entries of the first dictionary whose keys are in the second."""
|
|
294
|
+
return Dict(
|
|
295
|
+
tuple(
|
|
296
|
+
(key, a.value)
|
|
297
|
+
for key, a, b in _walk(left, right)
|
|
298
|
+
if isinstance(a, Just) and isinstance(b, Just)
|
|
299
|
+
)
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def diff[K: Comparable, V, W](left: Dict[K, V], right: Dict[K, W]) -> Dict[K, V]:
|
|
304
|
+
"""Keep the entries of the first dictionary whose keys are not in the second."""
|
|
305
|
+
return Dict(
|
|
306
|
+
tuple(
|
|
307
|
+
(key, a.value)
|
|
308
|
+
for key, a, b in _walk(left, right)
|
|
309
|
+
if isinstance(a, Just) and isinstance(b, Nothing)
|
|
310
|
+
)
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def merge[K: Comparable, A, B, R](
|
|
315
|
+
left_step: Callable[[K, A, R], R],
|
|
316
|
+
both_step: Callable[[K, A, B, R], R],
|
|
317
|
+
right_step: Callable[[K, B, R], R],
|
|
318
|
+
left: Dict[K, A],
|
|
319
|
+
right: Dict[K, B],
|
|
320
|
+
initial: R,
|
|
321
|
+
) -> R:
|
|
322
|
+
"""Combine two dictionaries in the most general way.
|
|
323
|
+
|
|
324
|
+
The keys are visited from lowest to highest. `left_step` gets the keys that
|
|
325
|
+
are only in the left dictionary, `both_step` the keys in both, and
|
|
326
|
+
`right_step` the keys that are only in the right dictionary.
|
|
327
|
+
"""
|
|
328
|
+
accumulator = initial
|
|
329
|
+
for key, a, b in _walk(left, right):
|
|
330
|
+
if isinstance(a, Just) and isinstance(b, Just):
|
|
331
|
+
accumulator = both_step(key, a.value, b.value, accumulator)
|
|
332
|
+
elif isinstance(a, Just):
|
|
333
|
+
accumulator = left_step(key, a.value, accumulator)
|
|
334
|
+
elif isinstance(b, Just):
|
|
335
|
+
accumulator = right_step(key, b.value, accumulator)
|
|
336
|
+
return accumulator
|