istr-python 1.1.41__tar.gz → 1.1.42__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: istr-python
3
- Version: 1.1.41
3
+ Version: 1.1.42
4
4
  Summary: istr - strings you can count on
5
5
  Author-email: Ruud van der Ham <rt.van.der.ham@gmail.com>
6
6
  Project-URL: Homepage, https://github.com/salabim/istr
@@ -589,9 +589,6 @@ map(istr.join,filter(istr.prime, istr.combinations(range(10),2)))
589
589
  ==> istr('02'), istr('03'), ... istr('89')
590
590
  ```
591
591
 
592
- #### version 1.1.30 2026-05-19
593
-
594
-
595
592
  #### reverse an istr
596
593
 
597
594
  The method `reversed()` will return an istr with the reversed content:
@@ -576,9 +576,6 @@ map(istr.join,filter(istr.prime, istr.combinations(range(10),2)))
576
576
  ==> istr('02'), istr('03'), ... istr('89')
577
577
  ```
578
578
 
579
- #### version 1.1.30 2026-05-19
580
-
581
-
582
579
  #### reverse an istr
583
580
 
584
581
  The method `reversed()` will return an istr with the reversed content:
@@ -5,7 +5,7 @@
5
5
  # |_||___/ \__||_|
6
6
  # strings you can count on
7
7
 
8
- __version__ = "1.1.41"
8
+ __version__ = "1.1.42"
9
9
  import functools
10
10
  import itertools
11
11
  import types
@@ -727,19 +727,16 @@ class istr(str):
727
727
  return self[::-1]
728
728
 
729
729
  def ceil(self, divisible_by=1):
730
- if divisible_by <= 0:
731
- raise ValueError(f"step has to be >0, not {divisible_by}")
732
- if divisible_by != int(divisible_by):
733
- raise ValueError(f"step has to be an integer value, not {divisible_by}")
730
+ if divisible_by != int(divisible_by) or divisible_by<1:
731
+ raise ValueError(f"divisible_by has to be an integer value >=1 , not {divisible_by}")
734
732
  n = istr.interpret_as_float(self)
735
733
  return istr((math.ceil(n / divisible_by)) * divisible_by)
736
734
 
737
735
  def floor(self, divisible_by=1):
738
- if divisible_by <= 0:
739
- raise ValueError(f"step has to be >0, not {divisible_by}")
740
- if divisible_by != int(divisible_by):
741
- raise ValueError(f"step has to be an integer value, not {divisible_by}")
742
- return istr((math.floor(self / divisible_by)) * divisible_by)
736
+ if divisible_by != int(divisible_by) or divisible_by<1:
737
+ raise ValueError(f"divisible_by has to be an integer value >=1 , not {divisible_by}")
738
+ n = istr.interpret_as_float(self)
739
+ return istr((math.floor(n / divisible_by)) * divisible_by)
743
740
 
744
741
  def interpret_as_int(self):
745
742
  if isinstance(self, istr):
@@ -896,8 +893,9 @@ class istr(str):
896
893
  remainder = dividend - trial
897
894
 
898
895
  root = root * 10 + x
899
- lines.append(dividend)
900
- lines.append(trial)
896
+ if trial:
897
+ lines.append(dividend)
898
+ lines.append(trial)
901
899
 
902
900
  lines.append(remainder)
903
901
  lines = [root] + [number] + lines[1:]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: istr-python
3
- Version: 1.1.41
3
+ Version: 1.1.42
4
4
  Summary: istr - strings you can count on
5
5
  Author-email: Ruud van der Ham <rt.van.der.ham@gmail.com>
6
6
  Project-URL: Homepage, https://github.com/salabim/istr
@@ -589,9 +589,6 @@ map(istr.join,filter(istr.prime, istr.combinations(range(10),2)))
589
589
  ==> istr('02'), istr('03'), ... istr('89')
590
590
  ```
591
591
 
592
- #### version 1.1.30 2026-05-19
593
-
594
-
595
592
  #### reverse an istr
596
593
 
597
594
  The method `reversed()` will return an istr with the reversed content:
@@ -2,7 +2,6 @@ README.md
2
2
  pyproject.toml
3
3
  istr/LICENSE.txt
4
4
  istr/__init__.py
5
- istr/istr - Copy.py
6
5
  istr/istr.py
7
6
  istr_python.egg-info/PKG-INFO
8
7
  istr_python.egg-info/SOURCES.txt
@@ -10,7 +10,7 @@ authors = [
10
10
  { name = "Ruud van der Ham", email = "rt.van.der.ham@gmail.com" },
11
11
  ]
12
12
  description = "istr - strings you can count on"
13
- version = "1.1.41"
13
+ version = "1.1.42"
14
14
  readme = "README.md"
15
15
  requires-python = ">=3.10"
16
16
  dependencies = []
@@ -1138,6 +1138,10 @@ def test_ceil():
1138
1138
  assert istr.ceil(1000) == 1000
1139
1139
  assert istr.ceil(1000, 1) == 1000
1140
1140
  assert istr.ceil(1000.2, 1).equals(istr(1001))
1141
+ with pytest.raises(ValueError,match=re.escape(f"divisible_by has to be an integer value >=1")):
1142
+ istr(1000).ceil(0)
1143
+ with pytest.raises(ValueError,match=re.escape(f"divisible_by has to be an integer value >=1")):
1144
+ istr(1000).ceil(2.5)
1141
1145
 
1142
1146
 
1143
1147
  def test_floor():
@@ -1146,8 +1150,10 @@ def test_floor():
1146
1150
  assert istr(1000).floor(2) == 1000
1147
1151
  assert istr(1000).floor(3) == 999
1148
1152
  assert istr.floor(1000) == 1000
1149
- assert istr.floor(1000, 1) == 1000
1150
- assert istr.floor(1000.2, 1).equals(istr(1000))
1153
+ with pytest.raises(ValueError,match=re.escape(f"divisible_by has to be an integer value >=1")):
1154
+ istr(1000).floor(0)
1155
+ with pytest.raises(ValueError,match=re.escape(f"divisible_by has to be an integer value >=1")):
1156
+ istr(1000).floor(2.5)
1151
1157
 
1152
1158
 
1153
1159
  def test_tuple_join():
@@ -1259,6 +1265,8 @@ def test_long_sqrt():
1259
1265
  istr("0"),
1260
1266
  ]
1261
1267
  assert istr.long_sqrt(34**2) == [istr("34"), istr("1156"), istr("9"), istr("256"), istr("256"), istr("0")]
1268
+ assert istr.long_sqrt("001156") == [istr("34"), istr("1156"), istr("9"), istr("256"), istr("256"), istr("0")]
1269
+
1262
1270
  assert (
1263
1271
  istr.long_sqrt(34**2, as_str=True)
1264
1272
  == """\
@@ -1277,6 +1285,8 @@ def test_long_sqrt():
1277
1285
  def test_long_multiplication():
1278
1286
  assert istr.long_multiplication(1234, 567) == [istr("1234"), istr("567"), istr("8638"), istr("7404"), istr("6170"), istr("699678")]
1279
1287
  assert istr(1234).long_multiplication(567) == [istr("1234"), istr("567"), istr("8638"), istr("7404"), istr("6170"), istr("699678")]
1288
+ assert istr("01234").long_multiplication("0567") == [istr("1234"), istr("567"), istr("8638"), istr("7404"), istr("6170"), istr("699678")]
1289
+
1280
1290
  assert (
1281
1291
  istr.long_multiplication(1234, 567, as_str=True)
1282
1292
  == """\
@@ -1294,6 +1304,8 @@ def test_long_multiplication():
1294
1304
  def test_long_division():
1295
1305
  assert istr.long_division(1395, 45) == [istr("31"), istr("45"), istr("1395"), istr("135"), istr("45"), istr("45"), istr("0")]
1296
1306
  assert istr(1395).long_division(45) == [istr("31"), istr("45"), istr("1395"), istr("135"), istr("45"), istr("45"), istr("0")]
1307
+ assert istr.long_division(istr("01395"), istr("045")) == [istr("31"), istr("45"), istr("1395"), istr("135"), istr("45"), istr("45"), istr("0")]
1308
+
1297
1309
  assert (
1298
1310
  istr.long_division(1395, 45, as_str=True)
1299
1311
  == """\
@@ -1,1084 +0,0 @@
1
- # _ _
2
- # (_) ___ | |_ _ __
3
- # | |/ __|| __|| '__|
4
- # | |\__ \| |_ | |
5
- # |_||___/ \__||_|
6
- # strings you can count on
7
-
8
- __version__ = "1.1.33"
9
- import functools
10
- import itertools
11
- import types
12
- import sys
13
- import inspect
14
- import math
15
- import operator
16
- import copy
17
- import bisect
18
- import collections
19
- import numbers
20
-
21
- """
22
- Note: the changelog is in changelog.md
23
-
24
- You can view the changelog on www.salabim.org/istr/changelog
25
-
26
- The readme can be viewed on www.salabim.org/istr/
27
- """
28
-
29
- _0_to_Z = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
30
-
31
-
32
- class _range:
33
- """
34
- based on https://codereview.stackexchange.com/questions/229073/pure-python-range-implementation
35
- """
36
-
37
- def __init__(self, cls, start, stop, step, length, base, int_format, repr_mode):
38
- self.start, self.stop, self.step = process_start_stop_step_length(start, stop, step, length)
39
- if self.step == 0:
40
- raise ValueError(f"step must not be zero, not {self.step}")
41
- if self.step < 0:
42
- step_sign = -1
43
- else:
44
- step_sign = 1
45
- self._len = max(1 + (self.stop - self.start - step_sign) // self.step, 0)
46
- self.parent_cls = cls
47
- self.base = cls._base if base is None else base
48
- self.int_format = cls._int_format if int_format is None else int_format
49
- self.repr_mode = cls._repr_mode if repr_mode is None else repr_mode
50
- self.init_done = True
51
-
52
- def __setattr__(self, name, value):
53
- if getattr(self, "init_done", False):
54
- raise AttributeError()
55
- super().__setattr__(name, value)
56
-
57
- def __contains__(self, value):
58
- if isinstance(value, int):
59
- return self._index(value) != -1
60
- return any(n == value for n in self)
61
-
62
- def __eq__(self, other):
63
- if not isinstance(other, type(self)):
64
- return False
65
- if self._len != len(other):
66
- return False
67
- if self._len == 0:
68
- return True
69
- if self.start != other.start:
70
- return False
71
- if self[-1] == other[-1]:
72
- return True
73
- return False
74
-
75
- def __getitem__(self, index):
76
- def adjust_indices(length, start, stop, step):
77
- if step is None:
78
- step = 1
79
- else:
80
- step = int(step)
81
-
82
- if start is None:
83
- start = length - 1 if step < 0 else 0
84
- else:
85
- start = int(start)
86
- if start < 0:
87
- start += length
88
- if start < 0:
89
- start = -1 if step < 0 else 0
90
- elif start >= length:
91
- start = length - 1 if step < 0 else length
92
-
93
- if stop is None:
94
- stop = -1 if step < 0 else length
95
- else:
96
- stop = int(stop)
97
- if stop < 0:
98
- stop += length
99
- if stop < 0:
100
- stop = -1 if step < 0 else 0
101
- elif stop >= length:
102
- stop = length - 1 if step < 0 else length
103
-
104
- return start, stop, step
105
-
106
- if isinstance(index, slice):
107
- start, stop, step = adjust_indices(self._len, index.start, index.stop, index.step)
108
- return self.parent_cls.range(
109
- self.start + self.step * start,
110
- self.start + self.step * stop,
111
- self.step * step,
112
- base=self.base,
113
- int_format=self.int_format,
114
- repr_mode=self.repr_mode,
115
- )
116
- index = int(index)
117
- if index < 0:
118
- index += self._len
119
- if not 0 <= index < self._len:
120
- raise IndexError("range object index out of range")
121
- return self.parent_cls(self.start + self.step * index, base=self.base, int_format=self.int_format, repr_mode=self.repr_mode)
122
-
123
- def __hash__(self):
124
- if self._len == 0:
125
- return id(self.parent_cls.range)
126
- return hash((self._len, self.start, int(self[-1])))
127
-
128
- def __iter__(self):
129
- value = self.start
130
- if self.step > 0:
131
- while value < self.stop:
132
- yield self.parent_cls(value, base=self.base, int_format=self.int_format, repr_mode=self.repr_mode)
133
- value += self.step
134
- else:
135
- while value > self.stop:
136
- yield self.parent_cls(value, base=self.base, int_format=self.int_format, repr_mode=self.repr_mode)
137
- value += self.step
138
-
139
- def __len__(self):
140
- return self._len
141
-
142
- def __repr__(self):
143
- if self.step == 1:
144
- return f"{self.parent_cls.__name__}.range({self.start}, {self.stop})"
145
- return f"{self.parent_cls.__name__}.range({self.start}, {self.stop}, {self.step})"
146
-
147
- def __reversed__(self):
148
- return iter(self[::-1])
149
-
150
- def _index(self, value):
151
- index_mul_step = value - self.start
152
- if index_mul_step % self.step:
153
- return -1
154
- index = index_mul_step // self.step
155
- if 0 <= index < self._len:
156
- return index
157
- return -1
158
-
159
- def count(self, value):
160
- """
161
- Rangeobject.count(value) -> integer
162
- Return number of occurrences of value.
163
- """
164
- return sum(1 for n in self if int(n) == int(value))
165
-
166
- def index(self, value, start=0, stop=None):
167
- if start < 0:
168
- start = max(self._len + start, 0)
169
- if stop is None:
170
- stop = self._len
171
- if stop < 0:
172
- stop += self._len
173
-
174
- if isinstance(value, int):
175
- index = self._index(value)
176
- if start <= index < stop:
177
- return index
178
- raise ValueError(f"{value} is not in range")
179
-
180
- i = start
181
- n = self.start + self.step * i
182
- while i < stop:
183
- if n == int(value):
184
- return i
185
- i += 1
186
- n += self.step
187
- raise ValueError(f"{value} is not in range")
188
-
189
-
190
- class istr(str):
191
- """
192
- istr object
193
-
194
- Parameters
195
- ----------
196
- value : any
197
- if str, the value will to be interpreted as an int
198
- istr('8') ==> istr('8')
199
- if numeric, the value will be interpreted as an int
200
- istr(8) ==> istr('8')
201
- if str and starts with '=', the value will be retrieved from letter variables and other characters unprocessed, e.g.
202
- if a=4 and b=32, istr('=ab1') will be istr'('4321')
203
- if str and starts with ':=', the value will be retrieved from letter variables. And the corresponding value will be set, e.g.
204
- if a=4 and b=32, istr(':=ab') will be istr'('432') and ab will be assigned that value too.
205
- if a dict (or subtype of dict), the same type dict will be returned with all values istr'ed
206
- istr({0: 0, 1: 1, 2: 4}) ==> {0: istr('0'), 1: istr('1'), 2: istr('4')}
207
- if an iterator, the iterator will be mapped with istr
208
- istr(i * i for i in range(3)) ==> <map object>
209
- list(istr(i * i for i in range(3))) ==> [istr('0'), istr('1'), istr('4')]
210
- if an iterable, the same type will be returned with all elements istr'ed
211
- istr([0, 1, 4]) ==> [istr('0'), istr('1'), istr('4')]
212
- istr((0, 1, 4)) ==> (istr('0'), istr('1'), istr('4'))
213
- istr({0, 1, 4}) ==> {istr('4'), istr('0'), istr('1')} # or similar
214
- if a range, an istr.range instance will be returned
215
- istr(range(3)) ==> istr.range(3)
216
- list(istr(range(3))) ==> [istr('0'), istr('1'), istr('2')]
217
- len(istr(range(3))) ==> 3
218
- if an istr, the same istr will be returned istr(istr('2')) ==> istr('2')
219
-
220
- it is possible to give more than one parameter, in which case a tuple
221
- of the istrs of the parameters will be returned, which can be handy
222
- to multiple assign, e.g.
223
- a, b, c = istr(5, 6, 7) ==> a=istr('5') , b=istr('6'), c=istr('7')
224
- """
225
-
226
- __slots__ = ("_as_int", "_this_base", "_this_int_format", "_this_repr_mode")
227
-
228
- _int_format = ""
229
- _repr_mode = "istr"
230
- _base = 10
231
- _nan = object()
232
- _digits_cache = {}
233
-
234
- @classmethod
235
- def _to_base(cls, number, base):
236
- if number < 0:
237
- raise ValueError(f"negative numbers are not allowed for base {base}")
238
- result = ""
239
- while number:
240
- result += _0_to_Z[number % base]
241
- number //= base
242
- return result[::-1] or "0"
243
-
244
- @classmethod
245
- def _to_int(cls, value, base=10):
246
- try:
247
- if base != 10 and isinstance(value, str):
248
- return int(value, base)
249
- else:
250
- return int(value)
251
- except Exception:
252
- return cls._nan
253
-
254
- def __new__(cls, *value, namespace=None, base=None, int_format=None, repr_mode=None):
255
- base = cls._base if base is None else base
256
- int_format = cls._int_format if int_format is None else int_format
257
- repr_mode = cls._repr_mode if repr_mode is None else repr_mode
258
- if len(value) == 1:
259
- value = value[0] # normal case of 1 parameter
260
- elif len(value) == 0:
261
- raise TypeError("no parameter given")
262
-
263
- match value:
264
- case range():
265
- return cls.range(value.start, value.stop, value.step, base=base, int_format=int_format, repr_mode=repr_mode)
266
- case _range():
267
- return value
268
- case cls():
269
- if value.is_int():
270
- return cls(value._as_int, base=base, int_format=int_format, repr_mode=repr_mode)
271
- else:
272
- return copy.copy(value)
273
- case dict():
274
- return type(value)(
275
- (k, cls(v, base=base, int_format=int_format, repr_mode=repr_mode, namespace=get_namespace(namespace))) for k, v in value.items()
276
- )
277
-
278
- case _ if not isinstance(value, (str, type)) and hasattr(value, "__iter__"):
279
- if hasattr(value, "__next__"):
280
- return map(lambda v: cls(v, base=base, int_format=int_format, repr_mode=repr_mode, namespace=get_namespace(namespace)), value)
281
- return type(value)(map(lambda v: cls(v, base=base, int_format=int_format, repr_mode=repr_mode, namespace=get_namespace(namespace)), value))
282
-
283
- if isinstance(value, str) and (any(value.startswith(s) and value != s for s in ("=", ":="))):
284
- if value[0] == "=":
285
- value = str(cls.compose(value[1:], namespace=get_namespace(namespace)))
286
- else: # it's :=
287
- var_name = value[2:]
288
- if not var_name.isidentifier():
289
- raise ValueError(f"{var_name!r} is not a valid identifier")
290
- value = str(cls.compose(var_name, namespace=get_namespace(namespace)))
291
- get_namespace(namespace)[var_name] = cls(value)
292
- as_int = cls._to_int(value, base)
293
- if as_int is cls._nan or isinstance(value, str):
294
- as_str = value
295
- else:
296
- if int_format == "" or base != 10:
297
- if base == 10:
298
- as_str = str(as_int)
299
- else:
300
- as_str = cls._to_base(as_int, base)
301
- else:
302
- as_str = f"{as_int:{int_format}}"
303
-
304
- self = super().__new__(cls, as_str)
305
- self._as_int = as_int
306
- self._this_base = base
307
- self._this_int_format = int_format
308
- self._this_repr_mode = repr_mode
309
- return self
310
-
311
- def __iter__(self):
312
- yield from self.__class__(super().__iter__())
313
-
314
- def __hash__(self):
315
- return hash((self.__class__, str(self)))
316
-
317
- def __eq__(self, other):
318
- if isinstance(other, istr):
319
- if self.is_int() and other.is_int():
320
- return self._as_int == other._as_int
321
- if isinstance(other, str):
322
- return super().__eq__(other)
323
- if self.is_int():
324
- try:
325
- return self._as_int == int(other)
326
- except Exception:
327
- return False
328
- return False
329
-
330
- def __ne__(self, other):
331
- return not self == other
332
-
333
- def __repr__(self):
334
- match self._this_repr_mode:
335
- case "istr":
336
- return f"{self.__class__.__name__}({repr(str(self))})"
337
- case "int":
338
- return "?" if self._as_int is self._nan else repr(self._as_int)
339
- case _:
340
- return repr(str(self))
341
-
342
- def __bool__(self):
343
- if self.is_int():
344
- return bool(self._as_int)
345
- return bool(str(self))
346
-
347
- def _frepr(self, obj):
348
- # like repr, but if obj is an istr, the as_repr is not used to make sure the
349
- # the returned value is istr(...) and not infuenced by the repr mode
350
- if isinstance(obj, self.__class__):
351
- return f"{obj.__class__.__name__}({super(istr, obj).__repr__()})"
352
- return repr(obj)
353
-
354
- def _int_method(self, name, op, *args):
355
- if len(args) == 1:
356
- other = args[0]
357
- if not self.is_int() or self._to_int(other) is self._nan:
358
- if name.startswith("__r"):
359
- raise TypeError(f"unsupported operand for {op}: {self._frepr(other)} and {self._frepr(self)}")
360
- else:
361
- raise TypeError(f"unsupported operand for {op}: {self._frepr(self)} and {self._frepr(other)}")
362
- if "<" in op or ">" in op:
363
- return getattr(self._as_int, name)(self._to_int(other))
364
- else:
365
- return self.__class__(getattr(self._as_int, name)(self._to_int(other)))
366
- else:
367
- if not self.is_int():
368
- raise TypeError(f"unsupported operand for {op}: {self._frepr(self)}")
369
- return self.__class__(getattr(self._as_int, name)())
370
-
371
- for name_op in (
372
- "__add__+ __radd__+ __sub__- __rsub__- __mul__* __rmul__* __floordiv__// __rfloordiv__// "
373
- "__truediv__/ __rtruediv__/ __pow__** __rpow__** __mod__% __rmod__% "
374
- "__divmod__divmod __rdivmod__divmod "
375
- "__le__<= __lt__< __gt__> __ge__>= "
376
- "__round__round __trunc__trunc __floor__floor __ceil__ceil __neg__- __pos__+ "
377
- "__invert__~ __abs__abs "
378
- ).split():
379
- i = len(name_op) - "".join(reversed(name_op)).find("_") # pos of last _
380
- name = name_op[:i]
381
- op = name_op[i:]
382
-
383
- locals()[name] = functools.partialmethod(_int_method, name, op)
384
-
385
- def __int__(self):
386
- if not self.is_int():
387
- raise ValueError(f"invalid literal for int(): {self._frepr(self)}")
388
- return int(self._as_int)
389
-
390
- def __float__(self):
391
- if not self.is_int():
392
- raise ValueError(f"invalid literal for float(): {self._frepr(self)}")
393
- return float(self._as_int)
394
-
395
- def __complex__(self):
396
- if not self.is_int():
397
- raise ValueError(f"invalid literal for complex(): {self._frepr(self)}")
398
- return complex(self._as_int)
399
-
400
- def is_even(self):
401
- return istr.is_divisible_by(self, 2)
402
-
403
- def is_odd(self):
404
- return not istr.is_divisible_by(self, 2)
405
-
406
- def is_palindrome(self):
407
- self_as_str = istr.interpret_as_str(self)
408
- return self_as_str == self_as_str[::-1]
409
-
410
- def is_non_decreasing(self):
411
- self_as_str = istr.interpret_as_str(self)
412
- return all(i0 <= i1 for i0, i1 in zip(self_as_str, self_as_str[1:]))
413
-
414
- def is_non_increasing(self):
415
- self_as_str = istr.interpret_as_str(self)
416
- return all(i0 >= i1 for i0, i1 in zip(self_as_str, self_as_str[1:]))
417
-
418
- def is_increasing(self):
419
- self_as_str = istr.interpret_as_str(self)
420
- return all(i0 < i1 for i0, i1 in zip(self_as_str, self_as_str[1:]))
421
-
422
- def is_decreasing(self):
423
- self_as_str = istr.interpret_as_str(self)
424
- return all(i0 > i1 for i0, i1 in zip(self_as_str, self_as_str[1:]))
425
-
426
- def is_divisible_by(self, divisor):
427
- return istr.divided_by(self, divisor) is not None
428
-
429
- def divided_by(self, divisor, fallback=None):
430
- if divisor == 0:
431
- return fallback
432
- quotient, remainder = divmod(istr.interpret_as_int(self), int(divisor))
433
- return istr(quotient) if remainder == 0 else fallback
434
-
435
- def is_prime(self):
436
- n = istr.interpret_as_int(self)
437
- if n < 1_000_000:
438
- return n in istr._primes_up_to_1_000_000_as_set()
439
-
440
- if not n & 1:
441
- return False
442
-
443
- for x in range(3, int(n**0.5) + 1, 2):
444
- if n % x == 0:
445
- return False
446
- return True
447
-
448
- @classmethod
449
- def primes(cls, start=None, stop=None, /, length=None, cache=True):
450
- """
451
- returns all primes up to a given upperbound or between a given lowerbound and upperbound
452
- alternatively, the length can be given
453
- """
454
- start, stop, step = process_start_stop_step_length(start, stop, 1, length)
455
- if (cls, "primes", start, stop) in _cache:
456
- return _cache[cls, "primes", start, stop]
457
-
458
- if stop <= 1_000_000:
459
- result = in_range(cls._primes_up_to_1_000_000(), start, stop)
460
- else:
461
- result = cls._primes(start, stop)
462
- if cache:
463
- _cache[cls, "primes", start, stop] = result
464
- return result
465
-
466
- @classmethod
467
- def _primes(cls, start, stop):
468
- sieve = bytearray(b"\x01") * (stop + 1)
469
- sieve[0:2] = b"\x00\x00"
470
-
471
- for i in range(2, int(stop**0.5) + 1):
472
- if sieve[i]:
473
- sieve[i * i : stop + 1 : i] = b"\x00" * (((stop - i * i) // i) + 1)
474
-
475
- return list(map(cls, ([i for i, is_prime in enumerate(sieve) if is_prime and start <= i < stop]))) # range check just to be sure
476
-
477
- @classmethod
478
- @functools.lru_cache
479
- def _primes_up_to_1_000_000(cls):
480
- return cls._primes(0, 1_000_000)
481
-
482
- @classmethod
483
- @functools.lru_cache
484
- def _primes_up_to_1_000_000_as_set(cls):
485
- return set(map(int, cls._primes_up_to_1_000_000()))
486
-
487
- def is_square(self):
488
- return istr.is_power_of(self, 2)
489
-
490
- def is_cube(self):
491
- return istr.is_power_of(self, 3)
492
-
493
- def is_power_of(self, exponent):
494
- n = istr.interpret_as_int(self)
495
- exponent = check_integer(exponent, "exponent")
496
- if n < 0:
497
- if exponent % 2 == 0:
498
- return False
499
- else:
500
- n = -n
501
- match exponent:
502
- case 0:
503
- return n == 1
504
- case 1:
505
- return True
506
- case x if x < 0:
507
- raise ValueError(f"exponent must be >=0; not {exponent}")
508
- case _ if n < 1_000_000:
509
- return n in istr._power_ofs_up_to_1_000_000_as_set(exponent)
510
- case _:
511
- ...
512
- return n == round(n ** (1 / exponent)) ** exponent
513
-
514
- @classmethod
515
- def squares(cls, start=None, stop=None, /, length=None, cache=True):
516
- """
517
- returns all squares up to a given stop or between a given start and stop
518
- alternatively, the length can be given
519
- """
520
- return cls.power_ofs(2, start, stop, length=length, cache=cache)
521
-
522
- @classmethod
523
- def cubes(cls, start=None, stop=None, /, length=None, cache=True):
524
- """
525
- returns all cubes up to a given stop or between a given start and stop
526
- alternatively, the length can be given
527
- """
528
- return cls.power_ofs(3, start, stop, length=length, cache=cache)
529
-
530
- @classmethod
531
- def power_ofs(cls, exponent, start=None, stop=None, /, length=None, cache=True):
532
- """
533
- returns all power of n up to a given stop or between a given start and stop
534
- alternatively, the length can be given
535
- """
536
- start, stop, step = process_start_stop_step_length(start, stop, 1, length)
537
- exponent = check_integer(exponent, "exponent")
538
-
539
- if (cls, "power_ofs", exponent, start, stop) in _cache:
540
- return _cache[cls, "power_ofs", exponent, start, stop]
541
- match exponent:
542
- case 0:
543
- if start <= 1 < stop:
544
- result = [istr(1)]
545
- else:
546
- result = []
547
- case 1:
548
- result = cls(list(range(start, stop)))
549
- case x if (x % 2 == 0 or start >= 0) and stop <= 1_000_000:
550
- result = in_range(cls._power_ofs_up_to_1_000_000(exponent), start, stop)
551
- case _:
552
- result = cls._power_ofs(exponent, start, stop)
553
- if cache:
554
- _cache[cls, "power_ofs", exponent, start, stop] = result
555
- return result
556
-
557
- @classmethod
558
- def _power_ofs(cls, exponent, start, stop):
559
- if exponent % 2 == 0:
560
- start = max(0, start)
561
- match exponent:
562
- case 0:
563
- if start <= 1 < stop:
564
- result = [1]
565
- else:
566
- result = []
567
- case 1:
568
- result = [*range(start, stop)]
569
- case _:
570
- result = []
571
- if start < 0: # can't be the case for even n (because of above limiting)
572
- i = -int((-start) ** (1 / exponent))
573
- else:
574
- i = int(start ** (1 / exponent))
575
- while (i_n := i**exponent) < stop:
576
- if i_n >= start: # just to be sure
577
- result.append(i_n)
578
- i += 1
579
-
580
- return list(map(cls, result))
581
-
582
- @classmethod
583
- @functools.lru_cache
584
- def _power_ofs_up_to_1_000_000(cls, n):
585
- return cls._power_ofs(n, 0, 1_000_000)
586
-
587
- @classmethod
588
- @functools.lru_cache
589
- def _power_ofs_up_to_1_000_000_as_set(cls, n):
590
- return set(map(int, cls._power_ofs_up_to_1_000_000(n)))
591
-
592
- def decompose(self, letters, namespace=None):
593
- """
594
- decompose one-letter variables into global variables
595
- each one-letter variable must represent just one character
596
- same one-letter variables represent the the same character
597
- the istr must have the same length as the letters
598
- """
599
- namespace = get_namespace(namespace)
600
-
601
- lookup = {}
602
-
603
- if len(letters) != len(self):
604
- raise ValueError(f"incorrect number of variables {len(letters)}; should be {len(self)}")
605
-
606
- for letter, ch in zip(letters, self):
607
- if letter in lookup and lookup[letter] != ch:
608
- raise ValueError(f"multiple values found for variable {letter}")
609
- if not letter.isidentifier():
610
- raise ValueError(f"{repr(letter)} cannot be used as a variable")
611
- lookup[str(letter)] = ch
612
- namespace.update(lookup)
613
-
614
- @classmethod
615
- def compose(cls, letters, namespace=None):
616
- """
617
- compose an istr from individual letter variables
618
- """
619
- namespace = get_namespace(namespace)
620
- result = []
621
- for letter in letters:
622
- if letter.isidentifier():
623
- if letter not in namespace:
624
- raise ValueError(f"variable {repr(letter)} not defined")
625
- result.append(str(namespace[letter]))
626
- else:
627
- result.append(letter)
628
- return cls("".join(result))
629
-
630
- def __or__(self, other):
631
- if isinstance(other, str):
632
- return self.__class__(str(self).__add__(other))
633
- else:
634
- raise TypeError(f"unsupported operand type(s) for |: {self._frepr(self)} and {self._frepr(other)}")
635
-
636
- def __ror__(self, other):
637
- if isinstance(other, str):
638
- return self.__class__(other.__add__(str(self)))
639
- else:
640
- raise TypeError(f"unsupported operand type(s) for |: {self._frepr(other)} and {self._frepr(self)}")
641
-
642
- def __matmul__(self, other):
643
- try:
644
- return self.__class__(super().__mul__(other))
645
- except Exception: # TypeError:
646
- raise TypeError(f"unsupported operand type(s) for @: {self._frepr(self)} and {self._frepr(other)}")
647
-
648
- def __rmatmul__(self, other):
649
- try:
650
- return self.__class__(super().__rmul__(other))
651
- except TypeError:
652
- raise TypeError(f"unsupported operand type(s) for @|: {self._frepr(other)} and {self._frepr(self)}")
653
-
654
- def __getitem__(self, key):
655
- return self.__class__(super().__getitem__(key))
656
-
657
- def all_distinct(self):
658
- return len(self) == len(set(self))
659
-
660
- def is_consecutive(self):
661
- s = istr.interpret_as_str(self)
662
- if len(s) <= 1:
663
- return False
664
- c0 = s[0]
665
- for c1 in s[1:]:
666
- if ord(c1) - ord(c0) != 1:
667
- return False
668
- c0 = c1
669
- return True
670
-
671
- def is_triangular(self):
672
- n = istr.interpret_as_int(self)
673
- if n <= 0:
674
- return False
675
- return istr.is_square(n * 8 + 1)
676
-
677
- def reversed(self):
678
- return self[::-1]
679
-
680
- def ceil(self, divisible_by=1):
681
- if divisible_by <= 0:
682
- raise ValueError(f"step has to be >0, not {divisible_by}")
683
- if divisible_by != int(divisible_by):
684
- raise ValueError(f"step has to be an integer value, not {divisible_by}")
685
- n = istr.interpret_as_float(self)
686
- return istr((math.ceil(n / divisible_by)) * divisible_by)
687
-
688
- def floor(self, divisible_by=1):
689
- if divisible_by <= 0:
690
- raise ValueError(f"step has to be >0, not {divisible_by}")
691
- if divisible_by != int(divisible_by):
692
- raise ValueError(f"step has to be an integer value, not {divisible_by}")
693
- return istr((math.floor(self / divisible_by)) * divisible_by)
694
-
695
- def interpret_as_int(self):
696
- if isinstance(self, istr):
697
- if not self.is_int():
698
- raise TypeError(f"not interpretable as int: {self._frepr(self)}")
699
- return self._as_int
700
- if isinstance(self, collections.abc.Iterable) and not isinstance(self, str):
701
- return int(istr.join(self))
702
-
703
- return int(self)
704
-
705
- def interpret_as_float(self):
706
- if isinstance(self, istr):
707
- if not self.is_int():
708
- raise TypeError(f"not interpretable as float: {self._frepr(self)}")
709
- return self._as_int
710
- if isinstance(self, collections.abc.Iterable) and not isinstance(self, str):
711
- return float(istr.join(self))
712
-
713
- return float(self)
714
-
715
- def interpret_as_str(self):
716
- if isinstance(self, collections.abc.Iterable) and not isinstance(self, str):
717
- return istr.join(self)
718
-
719
- return str(self)
720
-
721
- def _str_method(self, name, *args, **kwargs):
722
- return self.__class__(getattr(super(), name)(*args, **kwargs))
723
-
724
- for name in (
725
- "capitalize casefold center expandtabs format join ljust lower lstrip partition removeprefix "
726
- "removesuffix replace rjust rpartition rsplit rstrip split strip swapcase title translate upper zfill"
727
- ).split():
728
- locals()[name] = functools.partialmethod(_str_method, name)
729
-
730
- @classmethod
731
- def zip(cls, *iterables, strict=False, join=False):
732
- if join:
733
- return cls.concat(cls(zip(*iterables, strict=strict)))
734
- else:
735
- return cls(zip(*iterables, strict=strict))
736
-
737
- @classmethod
738
- def batched(cls, iterable, n, *, strict=False, join=False): # will be overridden if in itertools and not Python 3.12
739
- if n < 1:
740
- raise ValueError("n must be at least one")
741
- iterator = iter(iterable)
742
- while batch := tuple(itertools.islice(iterator, n)):
743
- if strict and len(batch) != n:
744
- raise ValueError("istr.batched(): incomplete batch")
745
- if join:
746
- yield cls.join(cls(batch))
747
- else:
748
- yield cls(batch)
749
-
750
- @classmethod
751
- def _itertools_method(cls, name, *args, **kwargs):
752
- return cls(getattr(itertools, name)(*args, **kwargs))
753
-
754
- @classmethod
755
- def _itertools_join_method(cls, name, *args, join=False, **kwargs):
756
- res = cls(getattr(itertools, name)(*args, **kwargs))
757
- return map(cls.join, res) if join else res
758
-
759
- for name in dir(itertools):
760
- if not name.startswith("_") and not name == "count": # count has its own method
761
- match name:
762
- case "groupby" | "tee":
763
- locals()[name] = getattr(itertools, name)
764
- case "permutations" | "combinations" | "combinations_with_replacement" | "product" | "batched" | "pairwise" | "zip_longest":
765
- if name == "batched" and sys.version_info[:2] == (3, 12):
766
- continue # version 3.12 does not support the strict parameter, so, we don't use the itertools method
767
- locals()[name] = functools.partialmethod(_itertools_join_method, name)
768
- case _:
769
- locals()[name] = functools.partialmethod(_itertools_method, name)
770
-
771
- def count(*args):
772
- if len(args) >= 2 and isinstance(args[0], istr):
773
- return str(args[0]).count(str(args[1]), *map(int, args[2:]))
774
- else:
775
- return istr(itertools.count(*args))
776
-
777
- def is_int(self):
778
- return self._as_int is not self._nan
779
-
780
- def join(self, iterable=None):
781
- if isinstance(self, istr):
782
- return self.__class__(str(self).join(iterable))
783
- if iterable is None:
784
- return istr("").join(self)
785
- if not isinstance(self, str):
786
- raise TypeError(f"{self!r} should be istr, str or iterable, not {type(self)}")
787
- return istr(self).join(iterable)
788
-
789
- @classmethod
790
- def concat(cls, iterable):
791
- return map(cls.join, cls(iterable))
792
-
793
- def prod(self, *, start=1):
794
- return math.prod(self, start=istr(start))
795
-
796
- @classmethod
797
- def sumprod(cls, p, q, /, strict=True):
798
- if "sumprod" in math.__dict__ and strict:
799
- return cls(math.sumprod(p, q))
800
- return sum(_map(operator.__mul__, cls(p), cls(q), strict=strict))
801
-
802
- @classmethod
803
- def enumerate(cls, iterable, start=0):
804
- for i, value in enumerate(iterable, int(start)):
805
- yield cls(i), value
806
-
807
- def this_base(self):
808
- return self._this_base
809
-
810
- def this_int_format(self):
811
- return self._this_int_format
812
-
813
- def this_repr_mode(self):
814
- return self._this_repr_mode
815
-
816
- @classmethod
817
- class int_format:
818
- def __new__(cls, cls_int_format, int_format=None):
819
- if int_format is None:
820
- return cls_int_format._int_format
821
- return super().__new__(cls)
822
-
823
- def __init__(self, cls, int_format):
824
- self.saved_int_format = cls._int_format
825
- self.saved_cls = cls
826
- if not (isinstance(int_format, str) and all(x in "0123456789" for x in int_format)):
827
- raise ValueError(f"{repr(int_format)} is incorrect int_format")
828
-
829
- cls._int_format = int_format
830
-
831
- def __enter__(self): ...
832
-
833
- def __exit__(self, exc_type, exc_value, exc_tb):
834
- self.saved_cls._int_format = self.saved_int_format
835
-
836
- @classmethod
837
- class repr_mode:
838
- def __new__(cls, cls_repr_mode, mode=None):
839
- if mode is None:
840
- return cls_repr_mode._repr_mode
841
- if mode is int:
842
- mode = "int"
843
- if mode in ("istr", "str", "int"): # istr is used only for TypeErrors
844
- return super().__new__(cls)
845
- raise TypeError(f"mode not 'istr', 'str' or 'int', but {repr(mode)}")
846
-
847
- def __init__(self, cls, mode):
848
- self.saved_repr_mode = cls._repr_mode
849
- self.saved_cls = cls
850
- cls._repr_mode = mode
851
-
852
- def __enter__(self): ...
853
-
854
- def __exit__(self, exc_type, exc_value, exc_tb):
855
- self.saved_cls._repr_mode = self.saved_repr_mode
856
-
857
- @classmethod
858
- class base:
859
- def __new__(cls, cls_base, base=None):
860
- if base is None:
861
- return cls_base._base
862
- if 2 <= base <= 36:
863
- return super().__new__(cls)
864
- raise ValueError(f"base not between 2 and 36, but {base}")
865
-
866
- def __init__(self, cls, base):
867
- self.saved_base = cls._base
868
- self.saved_cls = cls
869
- cls._base = base
870
-
871
- def __enter__(self): ...
872
-
873
- def __exit__(self, exc_type, exc_value, exc_tb):
874
- self.saved_cls._base = self.saved_base
875
-
876
- @classmethod
877
- def range(cls, start=None, stop=None, step=1, /, length=None, base=None, int_format=None, repr_mode=None):
878
- return _range(cls, start, stop, step, length, base=base, int_format=int_format, repr_mode=repr_mode)
879
-
880
- @classmethod
881
- def digits(cls, *args):
882
- """
883
- return an istr of istr'ed digits as specified with args
884
-
885
- if no args, 0-9 will be used
886
-
887
- all given args will be used
888
- each arg has to be either null string, <digit>, <digit>-<digit>, <digit>- or -<digit>
889
-
890
- the digits may be '0' through '9' and 'A' through 'Z' (not case sensitive)
891
- The returned value will always be in uppercase (if applicable).
892
-
893
- Examples
894
- --------
895
- istr.digits() ==> istr('0123456789')
896
- istr.digits('') ==> istr('0123456789')
897
- istr.digits('1') ==> istr('1')
898
- istr.digits('3-') ==> istr('3456789')
899
- istr.digits('-3') ==> istr('0123')
900
- istr('1-4', '6', '8-9') ==> istr('1234689')
901
- istr('1', '1-2', '1-3') ==> istr('11213')
902
- istr.digits('-z') ==> istr('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ')
903
- istr.digits('C') ==> istr('C')
904
- istr.digits('A-F') ==> istr('ABCDEF')
905
- istr.digits('X-') ==> istr('XYZ')
906
-
907
- Note
908
- ----
909
- A digit can occur more than once.
910
- """
911
- key = (args, cls._base, cls._int_format, cls._repr_mode)
912
- if key in cls._digits_cache:
913
- return cls._digits_cache[key]
914
- result = []
915
- if not args:
916
- args = ["0-9"]
917
- for arg in args:
918
- if arg.strip() == "":
919
- arg = "0-9"
920
- pre, *post = arg.split("-", 1)
921
- if pre.strip() == "":
922
- pre = "0"
923
- pre = pre.upper()
924
- if len(pre) > 1 or pre not in _0_to_Z:
925
- raise ValueError(f"incorrect specifier: {repr(arg)}")
926
- start = _0_to_Z.index(pre)
927
-
928
- if post:
929
- post = post[0]
930
- if post.strip() == "":
931
- if pre in "0123456789":
932
- post = "9"
933
- else:
934
- post = "Z"
935
- post = post.upper()
936
- if len(post) > 1 or post not in _0_to_Z:
937
- raise ValueError(f"incorrect specifier: {repr(arg)}")
938
- stop = _0_to_Z.index(post)
939
- if start > stop:
940
- raise ValueError(f"incorrect specifier: {repr(arg)}")
941
- else:
942
- stop = start
943
- result.extend(_0_to_Z[i] for i in range(start, stop + 1))
944
-
945
- result = cls("".join(result))
946
- cls._digits_cache[key] = result
947
- return result
948
-
949
-
950
- def _map(func, *iterables, strict=False):
951
- """
952
- like map, but with a strict parameter (also for Python < 3.14)
953
- """
954
- if sys.version_info >= (3, 14):
955
- yield from map(func, *iterables, strict=strict)
956
- return
957
-
958
- if not strict:
959
- yield from map(func, *iterables)
960
- return
961
-
962
- iterators = [iter(it) for it in iterables]
963
-
964
- while True:
965
- values = []
966
- exhausted = []
967
- for it in iterators:
968
- try:
969
- v = next(it)
970
- values.append(v)
971
- exhausted.append(False)
972
- except StopIteration:
973
- values.append(None)
974
- exhausted.append(True)
975
-
976
- if all(exhausted):
977
- return
978
-
979
- if any(exhausted) and not all(exhausted):
980
- raise ValueError("map_strict: iterables have different lengths")
981
-
982
- yield func(*values)
983
-
984
-
985
- _cache = {}
986
-
987
-
988
- def in_range(lst, start, stop):
989
- """
990
- this function will give all values of lst in the range [start, stop)
991
-
992
- Parameters
993
- ----------
994
- lst : list
995
- must be sorted!
996
-
997
- start : int (or istr)
998
- lowerbound
999
-
1000
- stop : int (or istr)
1001
- non inclusive upperbound
1002
-
1003
- Returns
1004
- -------
1005
- list of all items in [start, stop)
1006
-
1007
- Note
1008
- ----
1009
- Equivalent to
1010
- [item for item in lst if start <= item < stop], but more efficient.
1011
- """
1012
- start = int(start)
1013
- stop = int(stop)
1014
- left = bisect.bisect_left(lst, start)
1015
- right = bisect.bisect_left(lst, stop)
1016
- return lst[left:right]
1017
-
1018
-
1019
- def check_integer(value, value_description):
1020
-
1021
- if not isinstance(value, numbers.Number):
1022
- try:
1023
- return int(value)
1024
- except TypeError:
1025
- raise ValueError(f"{value_description} should be an integer, not {value}")
1026
-
1027
- if value != int(value):
1028
- raise ValueError(f"{value_description} should be an integer, not {value}")
1029
- return int(value)
1030
-
1031
-
1032
- def process_start_stop_step_length(start, stop, step, length):
1033
- if start is None and length is None:
1034
- raise ValueError("no bound(s) or length specified")
1035
- if length is not None:
1036
- if start is not None:
1037
- raise ValueError("both bound(s) and length specified")
1038
- length = check_integer(length, "length")
1039
- if length < 1:
1040
- raise ValueError(f"length must be >=1, not {length}")
1041
- start = 10 ** (length - 1)
1042
- stop = start * 10
1043
- else:
1044
- start, stop = (0, start) if stop is None else (start, stop)
1045
- start = check_integer(start, "start")
1046
- stop = check_integer(stop, "upperbound")
1047
- step = check_integer(step, "step")
1048
- return start, stop, step
1049
-
1050
-
1051
- def get_namespace(namespace):
1052
- if namespace is None:
1053
- frame = real_caller_frame()
1054
- namespace = frame.f_globals
1055
- return namespace
1056
-
1057
-
1058
- def real_caller_frame():
1059
- # this will return the frame of the first frame on the stack that does not belong to this module,
1060
- # so in the 'user' space
1061
- frame = inspect.currentframe()
1062
- frame_name = frame.f_globals.get("__name__")
1063
- frame = frame.f_back
1064
- while frame is not None and frame_name == frame.f_globals.get("__name__"):
1065
- frame = frame.f_back
1066
- return frame
1067
-
1068
-
1069
- istr.type = type(istr(0))
1070
-
1071
-
1072
- class istrModule(types.ModuleType):
1073
- def __call__(self, *args, **kwargs):
1074
- return istr(*args, **kwargs)
1075
-
1076
- def __setattr__(self, item, value):
1077
- setattr(istr, item, value)
1078
-
1079
- def __getattr__(self, item):
1080
- return getattr(istr, item)
1081
-
1082
-
1083
- if __name__ != "__main__":
1084
- sys.modules["istr"].__class__ = istrModule
File without changes