daatypes 0.1.0__tar.gz

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.
@@ -0,0 +1,218 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[codz]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py.cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ # Pipfile.lock
96
+
97
+ # UV
98
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ # uv.lock
102
+
103
+ # poetry
104
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
106
+ # commonly ignored for libraries.
107
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108
+ # poetry.lock
109
+ # poetry.toml
110
+
111
+ # pdm
112
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
113
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
114
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
115
+ # pdm.lock
116
+ # pdm.toml
117
+ .pdm-python
118
+ .pdm-build/
119
+
120
+ # pixi
121
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
122
+ # pixi.lock
123
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
124
+ # in the .venv directory. It is recommended not to include this directory in version control.
125
+ .pixi
126
+
127
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
128
+ __pypackages__/
129
+
130
+ # Celery stuff
131
+ celerybeat-schedule
132
+ celerybeat.pid
133
+
134
+ # Redis
135
+ *.rdb
136
+ *.aof
137
+ *.pid
138
+
139
+ # RabbitMQ
140
+ mnesia/
141
+ rabbitmq/
142
+ rabbitmq-data/
143
+
144
+ # ActiveMQ
145
+ activemq-data/
146
+
147
+ # SageMath parsed files
148
+ *.sage.py
149
+
150
+ # Environments
151
+ .env
152
+ .envrc
153
+ .venv
154
+ env/
155
+ venv/
156
+ ENV/
157
+ env.bak/
158
+ venv.bak/
159
+
160
+ # Spyder project settings
161
+ .spyderproject
162
+ .spyproject
163
+
164
+ # Rope project settings
165
+ .ropeproject
166
+
167
+ # mkdocs documentation
168
+ /site
169
+
170
+ # mypy
171
+ .mypy_cache/
172
+ .dmypy.json
173
+ dmypy.json
174
+
175
+ # Pyre type checker
176
+ .pyre/
177
+
178
+ # pytype static type analyzer
179
+ .pytype/
180
+
181
+ # Cython debug symbols
182
+ cython_debug/
183
+
184
+ # PyCharm
185
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
186
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
187
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
188
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
189
+ # .idea/
190
+
191
+ # Abstra
192
+ # Abstra is an AI-powered process automation framework.
193
+ # Ignore directories containing user credentials, local state, and settings.
194
+ # Learn more at https://abstra.io/docs
195
+ .abstra/
196
+
197
+ # Visual Studio Code
198
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
199
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
200
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
201
+ # you could uncomment the following to ignore the entire vscode folder
202
+ # .vscode/
203
+ # Temporary file for partial code execution
204
+ tempCodeRunnerFile.py
205
+
206
+ # Ruff stuff:
207
+ .ruff_cache/
208
+
209
+ # PyPI configuration file
210
+ .pypirc
211
+
212
+ # Marimo
213
+ marimo/_static/
214
+ marimo/_lsp/
215
+ __marimo__/
216
+
217
+ # Streamlit
218
+ .streamlit/secrets.toml
daatypes-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Lalremruata Chongmang
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,22 @@
1
+ Metadata-Version: 2.4
2
+ Name: daatypes
3
+ Version: 0.1.0
4
+ Summary: i simulate datatypes that i find useful
5
+ Project-URL: Homepage, https://github.com/deftasparagusanaconda/daatypes
6
+ Project-URL: Repository, https://github.com/deftasparagusanaconda/daatypes
7
+ Project-URL: Issues, https://github.com/deftasparagusanaconda/daatypes/issues
8
+ Author: deftasparagusanaconda
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: math,numerical
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
19
+ Requires-Python: >=3.10
20
+ Description-Content-Type: text/markdown
21
+
22
+ a collection of datatypes (or, rather, simulations thereof) that i find useful
daatypes-0.1.0/idfk.py ADDED
@@ -0,0 +1,415 @@
1
+ from __future__ import annotations
2
+ import math, collections, numbers
3
+ from daacorations import pretty_repr
4
+ from typing import Any, Literal
5
+ from numbers import Integral, Rational
6
+ from functools import cached_property
7
+ from itertools import pairwise, cycle
8
+ from fractions import Fraction
9
+ from abc import abstractmethod
10
+ from frozendefaultdict import frozendefaultdict
11
+ from collections import defaultdict
12
+ from collections.abc import Hashable, MutableMapping
13
+ from frozendict import frozendict
14
+ from frozendefaultdict import HashableMapping
15
+
16
+ # numbers ----------------------------------------------------------------------
17
+
18
+ class Natural(numbers.Integral):
19
+ 'a non-negative integer'
20
+
21
+ def __init__(self, value: Integral):
22
+ if value < 0 or not isinstance(value, Integral):
23
+ raise ValueError('must be a non-negative integer')
24
+ self.value: Integral = value
25
+
26
+ def __add__(self, other) -> Natural:
27
+ return type(self)(self.value + other.value)
28
+ def __sub__(self, other) -> Natural:
29
+ if other.value > self.value:
30
+ raise OutOfDomainError('')
31
+ return type(self)(self.value - other.value)
32
+ def __mul__(self, other) -> Natural:
33
+ return type(self)(self.value * other.value)
34
+ def __truediv__(self, other) -> Natural | Rational:
35
+ if self.value % other.value == 0:
36
+ return type(self)(self.value // other.value)
37
+ else:
38
+ return Fraction(self.value, other.value)
39
+ def __pow__(self, other) -> Natural:
40
+ if self.value == 0 and other.value == 0:
41
+ raise ArithmeticError('0 ** 0 is not defined')
42
+ return Natural(self.value ** other.value)
43
+ def __radd__(self, other) -> Natural:
44
+ return Natural(self.value + other.value)
45
+ def __rsub__(self, other) -> Natural:
46
+ return Natural(self.value + other.value)
47
+ def __rmul__(self, other) -> Natural:
48
+ return Natural(self.value + other.value)
49
+ def __rdiv__(self, other) -> Natural:
50
+ return Natural(self.value + other.value)
51
+ def __rpow__(self, other) -> Natural:
52
+ return Natural(self.value + other.value)
53
+
54
+ __repr__ = pretty_repr
55
+
56
+ class Cardinal:
57
+ 'a number that represents the size/cardinality of a set'
58
+
59
+ ALEPH_NULL = object()
60
+
61
+ def __init__(self, value: Natural | Literal[ALEPH_NULL]):
62
+ self.value: Natural | Literal[ALEPH_NULL] = value
63
+
64
+ __repr__ = pretty_repr
65
+
66
+ def __str__(self) -> str:
67
+ return str(self.value)
68
+
69
+ # collections ------------------------------------------------------------------
70
+
71
+ class Sized:
72
+ 'like collections.abc.Sized but properly allowing cardinal number sizes instead of just non-negative integer sizes. thus, infinite collections of different cardinalities are possible'
73
+ @abstractmethod
74
+ def __len__(self) -> Cardinal:
75
+ ...
76
+
77
+ __repr__ = pretty_repr
78
+
79
+ class IterableContainer(collections.abc.Iterable, collections.abc.Container):
80
+ 'an iterable container with rich mixins: count, filter, __contains__. can be applied to Collection, Sequence, Set, and any other ABCs that derive both Iterable and Container.'
81
+
82
+ def count(self, stuff: Set[Any], complement: bool = False) -> Natural:
83
+ '"how many things of stuff does this sequence have?"'
84
+ return sum(thing not in stuff for thing in self) if complement else sum(thing in stuff for thing in self)
85
+
86
+ def filter(self, stuff: Set[Any], complement: bool = False) -> Sequence[Any]:
87
+ '"things of this sequence which are in stuff"'
88
+ return type(self)((thing for thing in self if thing not in stuff) if complement else (thing for thing in self if thing in stuff))
89
+
90
+ def __contains__(self, thing) -> bool:
91
+ return any(thing2 in thing for thing2 in self)
92
+
93
+ __repr__ = pretty_repr
94
+
95
+ class Collection(Sized, IterableContainer):
96
+ 'like collections.abc.Collection but allows infinite len and has rich mixins (from IterableContainer)'
97
+ ...
98
+
99
+ class Set(Collection):
100
+ """an interface for datatypes that represent a set: a collection of unique elements"""
101
+
102
+ class Mapping(Collection):
103
+ ...
104
+
105
+ class Function(Collection):
106
+ """an object that maps elements from the domain set to the codomain set.
107
+
108
+ __len__ returns how many pairs are defined on the function."""
109
+
110
+ def __init__(self, domain: Set[Any], codomain: Set[Any], pairs: Mapping[Any, Any]):
111
+ self.domain: Set[Any] = domain
112
+ self.codomain: Set[Any] = codomain
113
+ self.pairs: Mapping[Any, Any] = pairs
114
+
115
+ @property
116
+ def is_total(self):
117
+ return self.domain == self.pairs.keys()
118
+
119
+ class Sequence(Collection):
120
+ ...
121
+
122
+ class _CyclicView:
123
+ def __init__(self, sequence: Sequence):
124
+ self.sequence = Sequence
125
+
126
+ def __getitem__(self, index: int | slice) -> Any:
127
+ match index:
128
+ case int(): return self.sequence[index % len(self)]
129
+ case slice(): return type(self.sequence)(self[i % len(self)] for i in range(*index))
130
+ case _: raise IndexError('index must be int or slice')
131
+
132
+ def __len__(self) -> int:
133
+ return len(self.sequence)
134
+
135
+ __repr__ = pretty_repr
136
+
137
+ # there are two kinds of sequences i recognize: if the domain is dense, a .cover cannot be defined. if the domain is not dense, a .cover can be defined, and you can traverse more easily.
138
+ #
139
+ # a sequence is a triple: (domain, function, codomain), where:
140
+ # domain is a . if it is not dense, a cover can be defined, and the sequence can be traversed easily
141
+
142
+ class Wheel(collections.abc.Iterator):
143
+ """a wheel for generating primes. its iterator returns the residues of the given wheel size. you get diminishing returns as you go up in size:
144
+
145
+ >>> primes = [2,3,5,7,11,13,17,19,23]
146
+ >>> for i in range(len(primes)):
147
+ >>> size = math.prod(primes[:i])
148
+ >>> wheel_count = Wheel(size).cycle_size
149
+ >>> naïve_count = size
150
+ >>> efficiency = wheel_count / naïve_count
151
+ >>> print(size, f'{wheel_count}/{naïve_count}={efficiency:.2%}')
152
+ >>>
153
+ 1 1/1=100.00%
154
+ 2 1/2=50.00%
155
+ 6 2/6=33.33%
156
+ 30 8/30=26.67%
157
+ 210 48/210=22.86%
158
+ 2310 480/2310=20.78%
159
+ 30030 5760/30030=19.18%
160
+ 510510 92160/510510=18.05%
161
+ 9699690 1658880/9699690=17.10%
162
+ 223092870 36495360/223092870=16.36%
163
+ """
164
+
165
+ @staticmethod
166
+ def _generate_steps(size: int) -> Sequence[int]:
167
+ candidates = tuple(filter(lambda candidate: math.gcd(candidate, size) == 1, range(size)))
168
+ return [b - a for a, b in pairwise(candidates)] + [(candidates[0] - candidates[-1]) % size]
169
+
170
+ def __init__(self, factors: collections.abc.Iterable[int]):
171
+ 'if you pass non-primes as factors, you wont be able to track them at the start of your generator. thats on you, idiot. sorry, harsh words, sorry but if you do that youre kinda dumb.. a bit'
172
+ size = math.lcm(*factors)
173
+
174
+ if math.prod(factors) != size:
175
+ raise ValueError('factors must be coprime!')
176
+
177
+ self.size: int = size
178
+ self.candidate: int = 1
179
+
180
+ steps: Sequence[int] = Wheel._generate_steps(size)
181
+ self.cycle: cycle[int] = cycle(steps)
182
+ self.cycle_size: int = len(steps)
183
+
184
+ # initialize candidate properly
185
+ #for factor in factors: # indexitis
186
+ # next(self)
187
+
188
+ def __iter__(self):
189
+ return self
190
+
191
+ def __next__(self) -> int:
192
+ self.candidate += next(self.cycle)
193
+ return self.candidate
194
+
195
+ __repr__ = pretty_repr
196
+
197
+ class PrimesIterableContainer(IterableContainer):
198
+ """a singleton class representing the infinite sequence of prime numbers. you can do things like:
199
+
200
+ Primes = PrimeSequence()
201
+
202
+ Primes[0] == 2
203
+ Primes[1] == 3
204
+ Primes[2] == 5
205
+ Primes[3] == 7
206
+
207
+ assert 5 in Primes
208
+ assert 13 in Primes
209
+
210
+ primes = Primes[:10]
211
+
212
+ for prime in Primes[:100]:
213
+ print(prime)
214
+
215
+ uses a 2x3 wheel by default, thus only testing 6*1-1, 6*1+1, 6*2-1, 6*2+1, 6*3-1, 6*3+1, … for generation. larger wheels like 2x3x5, 2x3x5x7, … can be used for better efficiency (see Wheel class)
216
+ """
217
+
218
+ _cache: MutableSequence[Integral] = [2, 3]
219
+
220
+ def __init__(self, wheel_factors: Iterable[int] = [2, 3]):
221
+ self.wheel: Wheel = Wheel(wheel_factors)
222
+ self._cache.extend(wheel_factors[len(self._cache):])
223
+
224
+ def _extend_cache_by(self, count: int = 1) -> None:
225
+ 'grow cache by a certain .count of primes'
226
+
227
+ while count > 0:
228
+ candidate = next(self.wheel)
229
+
230
+ # "is candidate prime?"
231
+ if all(candidate % prime != 0 for prime in self._cache):
232
+ count -= 1
233
+ self._cache.append(candidate)
234
+
235
+ def __getitem__(self, index: slice | int):
236
+ if isinstance(index, slice):
237
+ self._extend_cache_by(index.stop - len(self._cache))
238
+ elif isinstance(index, int):
239
+ if index < 0:
240
+ raise ValueError('cannot use negative indices on an infinite sequence')
241
+ self._extend_cache_by(index + 1 - len(self._cache))
242
+ else:
243
+ raise TypeError('index must be either slice or int')
244
+
245
+ return self._cache[index]
246
+
247
+ def __iter__(self):
248
+ i = 0
249
+
250
+ while True:
251
+ yield self[i := i + 1]
252
+
253
+ @classmethod
254
+ def __contains__(cls, number: Natural) -> bool:
255
+ # number is known (from cache) to be prime
256
+ if number in cls._cache:
257
+ return True
258
+
259
+ # number is within cache
260
+ if cls._cache[-1] >= number:
261
+ return number in cls._cache
262
+
263
+ # number is not within cache. perform divisibility check
264
+ # this way, cache is generated up to ≥⌊√n⌋ instead of ≥n
265
+ limit: int = math.floor(math.sqrt(number))
266
+
267
+ for prime in Primes:
268
+ if prime > limit:
269
+ return True
270
+ if number % prime == 0:
271
+ return False
272
+
273
+ def index(self, number):
274
+ if number not in self:
275
+ raise numberError(f'{number} is not prime')
276
+
277
+ while self._cache[-1] < number:
278
+ self._extend_cache_by(1)
279
+
280
+ return self._cache.index(number)
281
+
282
+ __repr__ = pretty_repr
283
+
284
+ Primes = PrimesIterableContainer()
285
+
286
+ '''
287
+ # here we dont subclass Rational because surds are not rational!
288
+ class Surd(Real):
289
+ 'like how Integral is a sub(Natural, Natural) pair, and Rational is a div(Integral, Integral) pair, Surd is a root(Integral, Integral) pair. its cousin is the log(Integral, Integral) pair, which i have not named yet. the equivalence class is op(a, b) = op(c, d). so, for example, 2√2 = 4√4'
290
+
291
+ def __init__(self, base: Rational, degree: Rational):
292
+ self.base = base
293
+ self.degree = degree
294
+
295
+ def __float__(self) -> float:
296
+ return self.base ** (1 / self.degree)
297
+
298
+ def __mul__(self, other) -> Surd:
299
+ return self.base
300
+ '''
301
+
302
+ class Monzo(Hashable, Sequence):#, Rational):
303
+ """a datatype that represents rational number stored as prime factors — conceptually as a sparse sequence, implemented as a mapping, but has the interface of a sequence :)
304
+
305
+ examples:
306
+ Monzo(2) = [1⟩ = {0: 1} = 2¹
307
+ Monzo(3) = [0 1⟩ = {1: 1} = 3¹
308
+ Monzo(4) = [2⟩ = {0: 2} = 2²
309
+ Monzo(5) = [0 0 1⟩ = {2: 1} = 5¹
310
+ """
311
+
312
+ def __init__(self, factors: HashableMapping, sign: Literal[-1, 0, +1] = 1):
313
+ self.factors: HashableMapping = factors
314
+ self.sign: Literal[-1, 0, +1] = sign
315
+
316
+ @cached_property
317
+ def numerator(self) -> Integral:
318
+ return self.sign * math.prod(Primes[prime_index] ** exponent for prime_index, exponent in self.factors.items() if exponent > 0)
319
+
320
+ @cached_property
321
+ def denominator(self) -> Integral:
322
+ return math.prod(Primes[prime_index] ** -exponent for prime_index, exponent in self.factors.items() if exponent < 0)
323
+
324
+ @cached_property
325
+ def prime_factors(self) -> Mapping[Integral, Integral]:
326
+ return frozendefaultdict.from_items(int, ((Primes[prime_index], exponent) for prime_index, exponent in self.factors.items()))
327
+
328
+ # to support Sequence interface
329
+ def __getitem__(self, index: int) -> int:
330
+ return self.factors[index] if index in self.factors else 0
331
+ def __len__(self) -> Cardinal:
332
+ keys = self.factors.keys()
333
+ return max(keys) + 1 if len(keys) > 0 else 1
334
+ def __iter__(self):
335
+ yield from (self.factors[index] for index in range(len(self)))
336
+
337
+ # to support Rational interface
338
+ def __add__(self, other) -> Monzo:
339
+ 'a / b + c / d = (a * d + b * c) / (b * d)'
340
+ a, b, c, d = self.numerator, self.denominator, other.numerator, other.denominator
341
+ return cls.from_parts(a * d + b * c, b * d)
342
+ def __eq__(self, other) -> bool:
343
+ 'a / b = c / d'
344
+ a, b, c, d = self.numerator, self.denominator, other.numerator, other.denominator
345
+ return a * d == b * c
346
+ def __float__(self) -> float:
347
+ return self.numerator / self.denominator
348
+
349
+ @classmethod
350
+ def from_prime_factors(cls, prime_factors: Mapping, *args, **kwargs) -> Monzo:
351
+ return cls(frozendefaultdict.from_items(int, ((Primes.index(factor), exponent) for factor, exponent in prime_factors.items())), *args, **kwargs)
352
+
353
+ @staticmethod
354
+ def _prime_factorize(number: Natural) -> HashableMapping[int, int]:
355
+ factors: MutableMapping = defaultdict(int)
356
+
357
+ prime_index = 0
358
+
359
+ while number > 1:
360
+ prime = Primes[prime_index]
361
+
362
+ if number % prime == 0:
363
+ number /= prime
364
+ if prime_index in factors:
365
+ factors[prime_index] += 1
366
+ else:
367
+ factors[prime_index] = 1
368
+ else:
369
+ prime_index += 1
370
+
371
+ return frozendefaultdict.from_items(int, factors.items())
372
+
373
+ @classmethod
374
+ def from_parts(cls, numerator: Integral, denominator: Integral) -> Monzo:
375
+ sign: Integral = int(math.copysign(1.0, numerator * denominator))
376
+
377
+ numerator = Monzo._prime_factorize(numerator)
378
+ denominator = Monzo._prime_factorize(denominator)
379
+
380
+ factors: MutableMapping = defaultdict(int, numerator)
381
+
382
+ for prime_factor, exponent in denominator.items():
383
+ if prime_factor in factors:
384
+ factors[prime_factor] -= exponent
385
+ else:
386
+ factors[prime_factor] = -exponent
387
+
388
+ return cls(frozendefaultdict.from_items(int, factors.items()), sign)
389
+
390
+ @classmethod
391
+ def from_rational(cls, number: Rational) -> Monzo:
392
+ return cls.from_parts(number.numerator, number.denominator)
393
+
394
+ @classmethod
395
+ def from_sequence(cls, exponents: Sequence[Integral]) -> Monzo:
396
+ 'construct from a sequence. has to use reversed, len, and indexing'
397
+ # because we dont want [1⟩ from [1 0 0], not [1 0 0⟩
398
+ trailing_zero_count: int = 0
399
+ for exponent in reversed(exponents):
400
+ if exponent != 0:
401
+ break
402
+ trailing_zero_count += 1
403
+
404
+ items = enumerate(exponents if trailing_zero_count == 0 else exponents[:-trailing_zero_count])
405
+
406
+ return cls(frozendefaultdict.from_items(int, items))
407
+
408
+ def __hash__(self) -> int:
409
+ return hash(Fraction(self))
410
+
411
+ __repr__ = pretty_repr
412
+ def __str__(self) -> str:
413
+ return '[' + ' '.join(str(exponent) for exponent in self) + '⟩'
414
+
415
+ Rational.register(Monzo)