istr-python 1.1.43__tar.gz → 1.1.45__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.43
3
+ Version: 1.1.45
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
@@ -370,14 +370,20 @@ a, b, c = istr(*range(3)) ==> a=istr('0') , b=istr('1'), c=istr('2')
370
370
  ```
371
371
  #### Alternative way of range specification
372
372
 
373
- Instead of `istr(range(..))` it is possible to use `istr,range(..)` instead, like
373
+ Instead of `istr(range(..))` it is possible to use `istr.range(..)` instead, like
374
374
 
375
375
  `list(istr.range(3,6))` ==> `[istr('3'), istr('4'), istr('5')]`
376
376
 
377
- In contrast to the builtin `range`, `istr.range` supports a non keyword parameter `length`:
377
+ In contrast to the builtin `range`, `istr.range` supports a keyword parameter `length`:
378
378
 
379
379
  `list(istr.range(length=3))` is equivalent to `istr.range(100, 1000)`
380
380
 
381
+ On top of this, `istr.range` support a keyword parameter `distinct`, to return a generator that yields only numbers with distinct numbers:
382
+
383
+ `list(istr.range(98,104,distinct=True))` is `[istr('98'), istr('102'), istr('103')]`
384
+
385
+ For `istr.range(length=..., distict=True)` the generation is optimized (not just checking each possible value).
386
+
381
387
  #### square root, cubic root and nth root
382
388
  The methods `sqrt`, `cbrt`, `nth_root`provide the square root, cubic root and nth root of an istr. If there is no integer root, the fallback value (default `istr('0')` will be returned.
383
389
  ```
@@ -547,15 +553,15 @@ istr.divisors(18, False) ==> [istr('1'), istr('18'), istr('2'), istr('9'), istr(
547
553
 
548
554
  #### test whether all characters are distinct
549
555
 
550
- With the `all_distinct` method, it is possible to test whether all characters are distinct (i.e. no character appears more than once).
556
+ With the `is_distinct` method, it is possible to test whether all characters are distinct (i.e. no character appears more than once).
551
557
 
552
558
  ```
553
- istr('01234').all_distict() ==> True
554
- istr('012340').all_distict() ==> False
559
+ istr('01234').is_distict() ==> True
560
+ istr('012340').is_distict() ==> False
555
561
  n98 = istr(98)
556
562
  n100 = n98 + 2
557
- istr(n98).all_distinct() ==> True
558
- istr(n100).all_distinct() ==> False
563
+ istr(n98).is_distinct() ==> True
564
+ istr(n100).is_distinct() ==> False
559
565
  ```
560
566
  #### test if characters are consecutive
561
567
 
@@ -824,8 +830,17 @@ Alternatively, it is possible to get a list of all squares, cubes, power_ofs or
824
830
 
825
831
  `istr.squares (length=2)` returns a list of all squares of length 2, so between 10 and 99.
826
832
 
833
+ If the keyword `distinct` is True, all values that are not distinct, will be excluded, like:
834
+
835
+ `list(istr.primes(list(istr.primes(100,132,distinct=True))` is `[istr('103'), istr('107'), istr('109'), istr('127')]`, thus leaving out 101, 113 and 131
836
+
827
837
  Unless `cache=False` is specified, the query result is cached.
828
838
 
839
+ #### filter distinct_only
840
+
841
+ With `istr.distinct_only` any iterable can skip values that are not distinct:
842
+ `list(istr.distinct_only([10, 11, 100, 123, 'abc', 'aaa']))` is `[istr('10'), istr('123'), istr('abc')]`
843
+
829
844
  #### Safe indexing (getitem)
830
845
 
831
846
  The `istr.getitem` method is essentially, a safe version of indexing an istr.
@@ -1041,6 +1056,41 @@ Note that `str(istr(":="))` is ":=" and does not assign any value.
1041
1056
 
1042
1057
  Usually, composing and decomposing uses the globals namespace, but this can be overridden with the namespace parameter. See the test suite for details.
1043
1058
 
1059
+ #### Evaluating expressions with one letter variables
1060
+ The `istr.eval` method is essentially the same as `istr.compose`, but (safely) evaluates the result.
1061
+ E.g.
1062
+
1063
+ ```
1064
+ a = 5
1065
+ b = 2
1066
+ print(istr.eval('aab + ba')
1067
+ ```
1068
+ will print `577`
1069
+
1070
+ The operators +, -, * and / are supported.
1071
+ Comparisons (==, <=, <, >, >= are also supported. In that case, the result is also istr-ed, so istr('0') for False and istr('1') for True. E.g.
1072
+
1073
+ ```
1074
+ a = 5
1075
+ b = 2
1076
+ print(istr.eval('a > b')
1077
+ ```
1078
+ will print 1 (meaning True)
1079
+
1080
+ Note that leading zeroes are accepted. So, `istr.eval('03 + 7')` is `10`.
1081
+ Instead of ==, = can be used. So, `istr.eval('12 = 5 + 7')` is `1` (meaning True)
1082
+
1083
+ ##### Alternative method
1084
+
1085
+ If istr is called with a string starting with a `?`, the string without the leading `?` will be evaluated using the new `istr.eval` method. So, the above examples read:
1086
+ ```
1087
+ a = 5
1088
+ b = 2
1089
+ print(istr("?aab + ba':")
1090
+ print(istr("?a > b")
1091
+ ```
1092
+ with output `577` and `1` (meaning True)
1093
+
1044
1094
  #### Subclassing istr
1045
1095
 
1046
1096
  When a class is derived from istr, all methods will return that newly derived class.
@@ -357,14 +357,20 @@ a, b, c = istr(*range(3)) ==> a=istr('0') , b=istr('1'), c=istr('2')
357
357
  ```
358
358
  #### Alternative way of range specification
359
359
 
360
- Instead of `istr(range(..))` it is possible to use `istr,range(..)` instead, like
360
+ Instead of `istr(range(..))` it is possible to use `istr.range(..)` instead, like
361
361
 
362
362
  `list(istr.range(3,6))` ==> `[istr('3'), istr('4'), istr('5')]`
363
363
 
364
- In contrast to the builtin `range`, `istr.range` supports a non keyword parameter `length`:
364
+ In contrast to the builtin `range`, `istr.range` supports a keyword parameter `length`:
365
365
 
366
366
  `list(istr.range(length=3))` is equivalent to `istr.range(100, 1000)`
367
367
 
368
+ On top of this, `istr.range` support a keyword parameter `distinct`, to return a generator that yields only numbers with distinct numbers:
369
+
370
+ `list(istr.range(98,104,distinct=True))` is `[istr('98'), istr('102'), istr('103')]`
371
+
372
+ For `istr.range(length=..., distict=True)` the generation is optimized (not just checking each possible value).
373
+
368
374
  #### square root, cubic root and nth root
369
375
  The methods `sqrt`, `cbrt`, `nth_root`provide the square root, cubic root and nth root of an istr. If there is no integer root, the fallback value (default `istr('0')` will be returned.
370
376
  ```
@@ -534,15 +540,15 @@ istr.divisors(18, False) ==> [istr('1'), istr('18'), istr('2'), istr('9'), istr(
534
540
 
535
541
  #### test whether all characters are distinct
536
542
 
537
- With the `all_distinct` method, it is possible to test whether all characters are distinct (i.e. no character appears more than once).
543
+ With the `is_distinct` method, it is possible to test whether all characters are distinct (i.e. no character appears more than once).
538
544
 
539
545
  ```
540
- istr('01234').all_distict() ==> True
541
- istr('012340').all_distict() ==> False
546
+ istr('01234').is_distict() ==> True
547
+ istr('012340').is_distict() ==> False
542
548
  n98 = istr(98)
543
549
  n100 = n98 + 2
544
- istr(n98).all_distinct() ==> True
545
- istr(n100).all_distinct() ==> False
550
+ istr(n98).is_distinct() ==> True
551
+ istr(n100).is_distinct() ==> False
546
552
  ```
547
553
  #### test if characters are consecutive
548
554
 
@@ -811,8 +817,17 @@ Alternatively, it is possible to get a list of all squares, cubes, power_ofs or
811
817
 
812
818
  `istr.squares (length=2)` returns a list of all squares of length 2, so between 10 and 99.
813
819
 
820
+ If the keyword `distinct` is True, all values that are not distinct, will be excluded, like:
821
+
822
+ `list(istr.primes(list(istr.primes(100,132,distinct=True))` is `[istr('103'), istr('107'), istr('109'), istr('127')]`, thus leaving out 101, 113 and 131
823
+
814
824
  Unless `cache=False` is specified, the query result is cached.
815
825
 
826
+ #### filter distinct_only
827
+
828
+ With `istr.distinct_only` any iterable can skip values that are not distinct:
829
+ `list(istr.distinct_only([10, 11, 100, 123, 'abc', 'aaa']))` is `[istr('10'), istr('123'), istr('abc')]`
830
+
816
831
  #### Safe indexing (getitem)
817
832
 
818
833
  The `istr.getitem` method is essentially, a safe version of indexing an istr.
@@ -1028,6 +1043,41 @@ Note that `str(istr(":="))` is ":=" and does not assign any value.
1028
1043
 
1029
1044
  Usually, composing and decomposing uses the globals namespace, but this can be overridden with the namespace parameter. See the test suite for details.
1030
1045
 
1046
+ #### Evaluating expressions with one letter variables
1047
+ The `istr.eval` method is essentially the same as `istr.compose`, but (safely) evaluates the result.
1048
+ E.g.
1049
+
1050
+ ```
1051
+ a = 5
1052
+ b = 2
1053
+ print(istr.eval('aab + ba')
1054
+ ```
1055
+ will print `577`
1056
+
1057
+ The operators +, -, * and / are supported.
1058
+ Comparisons (==, <=, <, >, >= are also supported. In that case, the result is also istr-ed, so istr('0') for False and istr('1') for True. E.g.
1059
+
1060
+ ```
1061
+ a = 5
1062
+ b = 2
1063
+ print(istr.eval('a > b')
1064
+ ```
1065
+ will print 1 (meaning True)
1066
+
1067
+ Note that leading zeroes are accepted. So, `istr.eval('03 + 7')` is `10`.
1068
+ Instead of ==, = can be used. So, `istr.eval('12 = 5 + 7')` is `1` (meaning True)
1069
+
1070
+ ##### Alternative method
1071
+
1072
+ If istr is called with a string starting with a `?`, the string without the leading `?` will be evaluated using the new `istr.eval` method. So, the above examples read:
1073
+ ```
1074
+ a = 5
1075
+ b = 2
1076
+ print(istr("?aab + ba':")
1077
+ print(istr("?a > b")
1078
+ ```
1079
+ with output `577` and `1` (meaning True)
1080
+
1031
1081
  #### Subclassing istr
1032
1082
 
1033
1083
  When a class is derived from istr, all methods will return that newly derived class.
@@ -5,7 +5,7 @@
5
5
  # |_||___/ \__||_|
6
6
  # strings you can count on
7
7
 
8
- __version__ = "1.1.43"
8
+ __version__ = "1.1.45"
9
9
  import functools
10
10
  import itertools
11
11
  import types
@@ -17,6 +17,8 @@ import copy
17
17
  import bisect
18
18
  import collections
19
19
  import numbers
20
+ import re
21
+ import ast
20
22
 
21
23
  """
22
24
  Note: the changelog is in changelog.md
@@ -282,15 +284,18 @@ class istr(str):
282
284
  return map(lambda v: cls(v, base=base, int_format=int_format, repr_mode=repr_mode, namespace=get_namespace(namespace)), value)
283
285
  return type(value)(map(lambda v: cls(v, base=base, int_format=int_format, repr_mode=repr_mode, namespace=get_namespace(namespace)), value))
284
286
 
285
- if isinstance(value, str) and (any(value.startswith(s) and value != s for s in ("=", ":="))):
286
- if value[0] == "=":
287
- value = str(cls.compose(value[1:], namespace=get_namespace(namespace)))
288
- else: # it's :=
289
- var_name = value[2:]
290
- if not var_name.isidentifier():
291
- raise ValueError(f"{var_name!r} is not a valid identifier")
292
- value = str(cls.compose(var_name, namespace=get_namespace(namespace)))
293
- get_namespace(namespace)[var_name] = cls(value)
287
+ if isinstance(value, str) and (any(value.startswith(s) and value != s for s in ("=", ":=",'?'))):
288
+ match value[0]:
289
+ case '?':
290
+ value = str(cls.eval(value[1:], namespace=get_namespace(namespace)))
291
+ case '=':
292
+ value = str(cls.compose(value[1:], namespace=get_namespace(namespace)))
293
+ case ':':
294
+ var_name = value[2:]
295
+ if not var_name.isidentifier():
296
+ raise ValueError(f"{var_name!r} is not a valid identifier")
297
+ value = str(cls.compose(var_name, namespace=get_namespace(namespace)))
298
+ get_namespace(namespace)[var_name] = cls(value)
294
299
  as_int = cls._to_int(value, base)
295
300
  if as_int is cls._nan or isinstance(value, str):
296
301
  as_str = value
@@ -487,21 +492,25 @@ class istr(str):
487
492
  return True
488
493
 
489
494
  @classmethod
490
- def primes(cls, start=None, stop=None, /, length=None, cache=True):
495
+ def primes(cls, start=None, stop=None, /, length=None, distinct=False, cache=True):
491
496
  """
492
497
  returns all primes up to a given upperbound or between a given lowerbound and upperbound
493
498
  alternatively, the length can be given
494
499
  """
495
500
  start, stop, step = process_start_stop_step_length(start, stop, 1, length)
496
- if (cls, "primes", start, stop) in _cache:
497
- return _cache[cls, "primes", start, stop]
501
+ if (cls, "primes", start, stop,distinct) in _cache:
502
+ return _cache[cls, "primes", start, stop,distinct]
498
503
 
499
504
  if stop <= 1_000_000:
500
505
  result = in_range(cls._primes_up_to_1_000_000(), start, stop)
501
506
  else:
502
507
  result = cls._primes(start, stop)
508
+ if distinct:
509
+ result=list(filter(cls.is_distinct,result))
510
+
503
511
  if cache:
504
- _cache[cls, "primes", start, stop] = result
512
+ _cache[cls, "primes", start, stop,distinct] = result
513
+
505
514
  return result
506
515
 
507
516
  @classmethod
@@ -555,23 +564,23 @@ class istr(str):
555
564
  return n == round(n ** (1 / exponent)) ** exponent
556
565
 
557
566
  @classmethod
558
- def squares(cls, start=None, stop=None, /, length=None, cache=True):
567
+ def squares(cls, start=None, stop=None, /, length=None, distinct=False, cache=True):
559
568
  """
560
569
  returns all squares up to a given stop or between a given start and stop
561
570
  alternatively, the length can be given
562
571
  """
563
- return cls.power_ofs(2, start, stop, length=length, cache=cache)
572
+ return cls.power_ofs(2, start, stop, length=length, distinct=distinct, cache=cache)
564
573
 
565
574
  @classmethod
566
- def cubes(cls, start=None, stop=None, /, length=None, cache=True):
575
+ def cubes(cls, start=None, stop=None, /, length=None, distinct=False,cache=True):
567
576
  """
568
577
  returns all cubes up to a given stop or between a given start and stop
569
578
  alternatively, the length can be given
570
579
  """
571
- return cls.power_ofs(3, start, stop, length=length, cache=cache)
580
+ return cls.power_ofs(3, start, stop, length=length, distinct=distinct,cache=cache)
572
581
 
573
582
  @classmethod
574
- def power_ofs(cls, exponent, start=None, stop=None, /, length=None, cache=True):
583
+ def power_ofs(cls, exponent, start=None, stop=None, /, length=None, distinct=None, cache=True):
575
584
  """
576
585
  returns all power of n up to a given stop or between a given start and stop
577
586
  alternatively, the length can be given
@@ -579,8 +588,8 @@ class istr(str):
579
588
  start, stop, step = process_start_stop_step_length(start, stop, 1, length)
580
589
  exponent = check_integer(exponent, "exponent")
581
590
 
582
- if (cls, "power_ofs", exponent, start, stop) in _cache:
583
- return _cache[cls, "power_ofs", exponent, start, stop]
591
+ if (cls, "power_ofs", exponent, start, stop,distinct) in _cache:
592
+ return _cache[cls, "power_ofs", exponent, start, stop,distinct]
584
593
  match exponent:
585
594
  case 0:
586
595
  if start <= 1 < stop:
@@ -593,8 +602,10 @@ class istr(str):
593
602
  result = in_range(cls._power_ofs_up_to_1_000_000(exponent), start, stop)
594
603
  case _:
595
604
  result = cls._power_ofs(exponent, start, stop)
605
+ if distinct:
606
+ result=list(filter(cls.is_distinct,result))
596
607
  if cache:
597
- _cache[cls, "power_ofs", exponent, start, stop] = result
608
+ _cache[cls, "power_ofs", exponent, start, stop, distinct] = result
598
609
  return result
599
610
 
600
611
  @classmethod
@@ -674,7 +685,20 @@ class istr(str):
674
685
  result.append(str(namespace[letter]))
675
686
  else:
676
687
  result.append(letter)
677
- return cls("".join(result))
688
+
689
+ result_str = "".join(result)
690
+ return cls(result_str)
691
+
692
+ @classmethod
693
+ def eval(cls, letters, namespace=None):
694
+ """
695
+ evaluates an istr from individual letter variables
696
+ """
697
+ expr = cls.compose(letters, namespace=namespace)
698
+ expr = re.sub(r"\b0+(\d+)\b", r"\1", expr) # delete leading zeroes
699
+ expr=re.sub(r'(?<![<>=!])=(?!=)', '==', expr) # support =
700
+ result = safe_eval(expr)
701
+ return cls(result)
678
702
 
679
703
  def __or__(self, other):
680
704
  if isinstance(other, str):
@@ -703,8 +727,12 @@ class istr(str):
703
727
  def __getitem__(self, key):
704
728
  return self.__class__(super().__getitem__(key))
705
729
 
706
- def all_distinct(self):
707
- return len(self) == len(set(self))
730
+ def is_distinct(self):
731
+ s = istr.interpret_as_str(self)
732
+ return len(s) == len(set(s))
733
+
734
+ all_distinct=is_distinct
735
+
708
736
 
709
737
  def is_consecutive(self):
710
738
  s = istr.interpret_as_str(self)
@@ -859,6 +887,7 @@ class istr(str):
859
887
  def enumerate(cls, iterable, start=0):
860
888
  for i, value in enumerate(iterable, int(start)):
861
889
  yield cls(i), value
890
+
862
891
 
863
892
  @staticmethod
864
893
  @functools.lru_cache(maxsize=1)
@@ -912,13 +941,26 @@ class istr(str):
912
941
  return istr._long_sqrt(self.number)
913
942
  case "line_lengths":
914
943
  return [*map(len, istr._long_sqrt(self.number))]
944
+ case "indents":
945
+ lines = self.output
946
+ result = []
947
+ for line in lines.splitlines():
948
+ if '-' not in line:
949
+ for i,c in enumerate(line[2:]):
950
+ if c != ' ':
951
+ break
952
+ else:
953
+ i=0
954
+ result.append(i)
955
+ return result
956
+
915
957
  case "output":
916
958
  lines = istr._long_sqrt(self.number)
917
959
  result = []
918
960
  extra = len(number) % 2 == 0
919
961
  result.append(f" {extra*' '}{' '.join(lines[0])}")
920
962
  result.append(f" {len(lines[1])*'-'}")
921
- result.append(f"\/{lines[1]}")
963
+ result.append(f"\\/{lines[1]}")
922
964
 
923
965
  last_len = len(lines[2])
924
966
  for i, line in enumerate(lines):
@@ -952,6 +994,8 @@ class istr(str):
952
994
 
953
995
  return lines
954
996
 
997
+
998
+
955
999
  def long_multiplication(self, number, as_str=False):
956
1000
  class NumbersHolder:
957
1001
  def __init__(self, number0, number1):
@@ -1139,7 +1183,18 @@ class istr(str):
1139
1183
  self.saved_cls._base = self.saved_base
1140
1184
 
1141
1185
  @classmethod
1142
- def range(cls, start=None, stop=None, step=1, /, length=None, base=None, int_format=None, repr_mode=None):
1186
+ def _range_distinct(cls,length):
1187
+ for c0 in range(1,10):
1188
+ for crest in itertools.permutations(itertools.chain(range(0,c0),range(c0+1,10)),length-1):
1189
+ yield cls(c0)|cls.join(cls(crest))
1190
+
1191
+ @classmethod
1192
+ def range(cls, start=None, stop=None, step=1, /, length=None, distinct=False, base=None, int_format=None, repr_mode=None):
1193
+ if distinct:
1194
+ if length is None:
1195
+ return filter(cls.is_distinct,_range(cls,start,stop,step,length,base=base, int_format=int_format, repr_mode=repr_mode))
1196
+ else:
1197
+ return cls._range_distinct(length)
1143
1198
  return _range(cls, start, stop, step, length, base=base, int_format=int_format, repr_mode=repr_mode)
1144
1199
 
1145
1200
  @classmethod
@@ -1330,6 +1385,54 @@ def real_caller_frame():
1330
1385
  return frame
1331
1386
 
1332
1387
 
1388
+ OPS = {ast.Add: operator.add, ast.Sub: operator.sub, ast.Mult: operator.mul, ast.Div: operator.truediv, ast.Pow: operator.pow}
1389
+
1390
+ COMPARE_OPS = {
1391
+ ast.Eq: operator.eq, # ==
1392
+ ast.NotEq: operator.ne, # !=
1393
+ ast.Lt: operator.lt, # <
1394
+ ast.LtE: operator.le, # <=
1395
+ ast.Gt: operator.gt, # >
1396
+ ast.GtE: operator.ge, # >=
1397
+ }
1398
+
1399
+
1400
+ def safe_eval(expr):
1401
+ def evaluate(node):
1402
+ if isinstance(node, ast.Constant) and isinstance(node.value, (int, float, str, bool)):
1403
+ return node.value
1404
+
1405
+ if isinstance(node, ast.BinOp) and type(node.op) in OPS:
1406
+ return OPS[type(node.op)](evaluate(node.left), evaluate(node.right))
1407
+
1408
+ if isinstance(node, ast.UnaryOp) and isinstance(node.op, (ast.UAdd, ast.USub)):
1409
+ value = evaluate(node.operand)
1410
+ return value if isinstance(node.op, ast.UAdd) else -value
1411
+
1412
+ if isinstance(node, ast.Compare):
1413
+ left = evaluate(node.left)
1414
+
1415
+ # Supports chained comparisons, e.g. 1 < 2 < 3
1416
+ for op, comparator in zip(node.ops, node.comparators):
1417
+ op_func = COMPARE_OPS.get(type(op))
1418
+ if op_func is None:
1419
+ raise ValueError("Unsupported comparison")
1420
+
1421
+ right = evaluate(comparator)
1422
+
1423
+ if not op_func(left, right):
1424
+ return False
1425
+
1426
+ left = right
1427
+
1428
+ return True
1429
+
1430
+ raise ValueError("Unsupported expression")
1431
+
1432
+ tree = ast.parse(expr, mode="eval")
1433
+ return evaluate(tree.body)
1434
+
1435
+
1333
1436
  istr.type = type(istr(0))
1334
1437
 
1335
1438
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: istr-python
3
- Version: 1.1.43
3
+ Version: 1.1.45
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
@@ -370,14 +370,20 @@ a, b, c = istr(*range(3)) ==> a=istr('0') , b=istr('1'), c=istr('2')
370
370
  ```
371
371
  #### Alternative way of range specification
372
372
 
373
- Instead of `istr(range(..))` it is possible to use `istr,range(..)` instead, like
373
+ Instead of `istr(range(..))` it is possible to use `istr.range(..)` instead, like
374
374
 
375
375
  `list(istr.range(3,6))` ==> `[istr('3'), istr('4'), istr('5')]`
376
376
 
377
- In contrast to the builtin `range`, `istr.range` supports a non keyword parameter `length`:
377
+ In contrast to the builtin `range`, `istr.range` supports a keyword parameter `length`:
378
378
 
379
379
  `list(istr.range(length=3))` is equivalent to `istr.range(100, 1000)`
380
380
 
381
+ On top of this, `istr.range` support a keyword parameter `distinct`, to return a generator that yields only numbers with distinct numbers:
382
+
383
+ `list(istr.range(98,104,distinct=True))` is `[istr('98'), istr('102'), istr('103')]`
384
+
385
+ For `istr.range(length=..., distict=True)` the generation is optimized (not just checking each possible value).
386
+
381
387
  #### square root, cubic root and nth root
382
388
  The methods `sqrt`, `cbrt`, `nth_root`provide the square root, cubic root and nth root of an istr. If there is no integer root, the fallback value (default `istr('0')` will be returned.
383
389
  ```
@@ -547,15 +553,15 @@ istr.divisors(18, False) ==> [istr('1'), istr('18'), istr('2'), istr('9'), istr(
547
553
 
548
554
  #### test whether all characters are distinct
549
555
 
550
- With the `all_distinct` method, it is possible to test whether all characters are distinct (i.e. no character appears more than once).
556
+ With the `is_distinct` method, it is possible to test whether all characters are distinct (i.e. no character appears more than once).
551
557
 
552
558
  ```
553
- istr('01234').all_distict() ==> True
554
- istr('012340').all_distict() ==> False
559
+ istr('01234').is_distict() ==> True
560
+ istr('012340').is_distict() ==> False
555
561
  n98 = istr(98)
556
562
  n100 = n98 + 2
557
- istr(n98).all_distinct() ==> True
558
- istr(n100).all_distinct() ==> False
563
+ istr(n98).is_distinct() ==> True
564
+ istr(n100).is_distinct() ==> False
559
565
  ```
560
566
  #### test if characters are consecutive
561
567
 
@@ -824,8 +830,17 @@ Alternatively, it is possible to get a list of all squares, cubes, power_ofs or
824
830
 
825
831
  `istr.squares (length=2)` returns a list of all squares of length 2, so between 10 and 99.
826
832
 
833
+ If the keyword `distinct` is True, all values that are not distinct, will be excluded, like:
834
+
835
+ `list(istr.primes(list(istr.primes(100,132,distinct=True))` is `[istr('103'), istr('107'), istr('109'), istr('127')]`, thus leaving out 101, 113 and 131
836
+
827
837
  Unless `cache=False` is specified, the query result is cached.
828
838
 
839
+ #### filter distinct_only
840
+
841
+ With `istr.distinct_only` any iterable can skip values that are not distinct:
842
+ `list(istr.distinct_only([10, 11, 100, 123, 'abc', 'aaa']))` is `[istr('10'), istr('123'), istr('abc')]`
843
+
829
844
  #### Safe indexing (getitem)
830
845
 
831
846
  The `istr.getitem` method is essentially, a safe version of indexing an istr.
@@ -1041,6 +1056,41 @@ Note that `str(istr(":="))` is ":=" and does not assign any value.
1041
1056
 
1042
1057
  Usually, composing and decomposing uses the globals namespace, but this can be overridden with the namespace parameter. See the test suite for details.
1043
1058
 
1059
+ #### Evaluating expressions with one letter variables
1060
+ The `istr.eval` method is essentially the same as `istr.compose`, but (safely) evaluates the result.
1061
+ E.g.
1062
+
1063
+ ```
1064
+ a = 5
1065
+ b = 2
1066
+ print(istr.eval('aab + ba')
1067
+ ```
1068
+ will print `577`
1069
+
1070
+ The operators +, -, * and / are supported.
1071
+ Comparisons (==, <=, <, >, >= are also supported. In that case, the result is also istr-ed, so istr('0') for False and istr('1') for True. E.g.
1072
+
1073
+ ```
1074
+ a = 5
1075
+ b = 2
1076
+ print(istr.eval('a > b')
1077
+ ```
1078
+ will print 1 (meaning True)
1079
+
1080
+ Note that leading zeroes are accepted. So, `istr.eval('03 + 7')` is `10`.
1081
+ Instead of ==, = can be used. So, `istr.eval('12 = 5 + 7')` is `1` (meaning True)
1082
+
1083
+ ##### Alternative method
1084
+
1085
+ If istr is called with a string starting with a `?`, the string without the leading `?` will be evaluated using the new `istr.eval` method. So, the above examples read:
1086
+ ```
1087
+ a = 5
1088
+ b = 2
1089
+ print(istr("?aab + ba':")
1090
+ print(istr("?a > b")
1091
+ ```
1092
+ with output `577` and `1` (meaning True)
1093
+
1044
1094
  #### Subclassing istr
1045
1095
 
1046
1096
  When a class is derived from istr, all methods will return that newly derived class.
@@ -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.43"
13
+ version = "1.1.45"
14
14
  readme = "README.md"
15
15
  requires-python = ">=3.10"
16
16
  dependencies = []
@@ -3,6 +3,8 @@ import itertools
3
3
  import os
4
4
  import sys
5
5
  import re
6
+ import time
7
+ from collections.abc import Iterator
6
8
 
7
9
  if __name__ == "__main__": # to make the tests run without the pytest cli
8
10
  import os, sys # three lines to use the local package and chdir
@@ -196,7 +198,30 @@ def test_range():
196
198
  with pytest.raises(ValueError, match=re.escape("both bound(s) and length specified")):
197
199
  istr.range(10, length=2)
198
200
 
199
-
201
+ assert list(map(int,istr.range(98,124,distinct=True)))==[98, 102, 103, 104, 105, 106, 107, 108, 109, 120, 123]
202
+ assert sorted(map(int,set(istr.range(length=2))-set(istr.range(length=2,distinct=True))))==[11, 22, 33, 44, 55, 66, 77, 88, 99]
203
+
204
+ assert type(istr.range(12)).__name__=='_range'
205
+ assert isinstance(istr.range(12,distinct=True),Iterator)
206
+ assert type(istr.range(length=2)).__name__=='_range'
207
+ assert isinstance(istr.range(length=2,distinct=True),Iterator)
208
+
209
+ t0=time.perf_counter()
210
+ result0=istr.range(length=4,distinct=True)
211
+ duration0=time.perf_counter()-t0
212
+ t0=time.perf_counter()
213
+ result1=istr.range(1000,10000,distinct=True)
214
+ duration1=time.perf_counter()-t0
215
+ assert list(result0)==list(result1)
216
+ assert duration1>duration0*2
217
+
218
+
219
+ def test_various_distinct():
220
+ assert sorted(set(istr.primes(80,140))-set(istr.primes(80,140,distinct=True)))==istr([101, 113, 131])
221
+ assert sorted(set(istr.squares(150))-set(istr.squares(150,distinct=True)))==istr([100, 121,144])
222
+ assert sorted(set(istr.cubes(10000))-set(istr.cubes(10000,distinct=True)))==istr([343, 1000, 1331,2744,3375,8000])
223
+ assert sorted(set(istr.power_ofs(4,50000))-set(istr.power_ofs(4,50000,distinct=True)))==istr([6561,10000,14641])
224
+
200
225
  def test_misc():
201
226
  assert istr("") == ""
202
227
  assert istr("") != 0
@@ -1128,8 +1153,26 @@ def test_decompose():
1128
1153
  istr(12).decompose("xyz")
1129
1154
  with pytest.raises(ValueError):
1130
1155
  istr(123).decompose("xy1")
1156
+
1157
+ def test_eval():
1158
+ global a,b,c
1159
+ a = 3
1160
+ b = istr('4')
1161
+ c=istr(7)
1162
+ assert istr.eval('a + b')==7
1163
+ assert istr.eval('a + b = c')
1164
+ assert istr.eval('a + b > 6')
1165
+ assert not istr.eval('a + b < 6')
1166
+ with pytest.raises(ValueError):
1167
+ assert istr.eval('d')
1131
1168
 
1132
-
1169
+ assert istr('?a + b')==7
1170
+ assert istr('?a + b = c')
1171
+ assert istr('?a + b > 6')
1172
+ assert not istr('?a + b < 6')
1173
+ with pytest.raises(ValueError):
1174
+ assert istr('?d')
1175
+
1133
1176
  def test_ceil():
1134
1177
  assert istr(1000).ceil() == 1000
1135
1178
  assert istr(1000).ceil(1) == 1000
File without changes