
The istr module makes it possible to use strings as if they were integers.
For the changelog, see www.salabim.org/istr/changelog .
Using strings as if they were integers can be very handy for solving puzzles, but also for other purposes. For instance, the famous send more money puzzle, where each letter has to be replaced by a unique digit (0-9)
xxxxxxxxxxS E N DM O R E--------- +M O N E Y
can be nicely, albeit not very efficient, coded as:
xxxxxxxxxximport istrfor s, e, n, d, m, o, r, y in istr.permutations(range(10), 8):if m and ((s|e|n|d) + (m|o|r|e) == (m|o|n|e|y):print(f' {s|e|n|d}')print(f' {m|o|r|e}')print('-----')print(f'{m|o|n|e|y}')
or even
xxxxxxxxxximport istrfor S, E, N, D, M, O, R, Y in istr.permutations(range(10), 8):if M and (istr("=SEND") + istr("=MORE")) == istr("=MONEY"):print(" " | istr("=SEND"))print(" " | istr("=MORE"))print("-----")print(istr("=MONEY"))
Of, if we want to add all the digits in a string:
xxxxxxxxxxsum_digits = sum(istr('9282334')) # answer 31
The module is also a demonstration of extending a class (str) with additional and modified functionality.
Installing istr with pip is easy.
xxxxxxxxxxpip install istr-python
or when you want to upgrade,
xxxxxxxxxxpip install istr-python --upgrade
Alternatively, istr.py can be just copied into your current work directory from GitHub (https://github.com/salabim/istr).
No dependencies!
Just start with
xxxxxxxxxximport istr
or the more conventional, more verbose:
xxxxxxxxxxfrom istr import istr
We can define an istr, like:
xxxxxxxxxxfour = istr('4')five = istr('5')
The variables four and five can now be used as if they were int:
xxxxxxxxxxtwenty = four * five
, after which twenty is istr('20')
The same can be done with
xxxxxxxxxxtwenty = 4 * five
or
xxxxxxxxxxtwenty = four * 5
And now twenty can be used as if it was an int as well. So
xxxxxxxxxxtwenty - four
is istr('16')
We can do all the usual arithmetic operations on istrs, e.g.
xxxxxxxxxx- four + (twenty / 2)
is istr('6')
And we can test for equality. So:
xxxxxxxxxxtwenty == 20
is True.
But istrs are actually strings! So
xxxxxxxxxxtwenty == '20'
is also True!
For the order comparisons (<=, <, >, >=), an istr is always interpreted as an int.
That means that
xxxxxxxxxxtwenty < 30twenty >= '10' # here '10' is converted to the integer 10 for the comparison
are bothTrue.
In contrast to an ordinary string
xxxxxxxxxxprint(four + five)
prints 9, as istr are treated as ints (if possible).
Please note that four could have also been initialized with
xxxxxxxxxxfour = istr(4)
or even
xxxxxxxxxxfour, five = istr(4, 5)
All calculations are strictly integer calculations. That means that if a float or decimal variable is ever produced, it will be converted to an int. Also, divisions are always floor divisions!
We should realize that istrs are in fact strings.
To concatenate two istrs (or an istr and a str), we cannot use the + operator (remember four + five is istr('9')).
To concatenate strings, we use the or operator (|). So
xxxxxxxxxxfour | five
will be istr(45`).
And
xxxxxxxxxx(four | five) / 3
is istr('9').
To repeat a string in the usual sense, you cannot use the * operator (remember 3 * four is istr('12').
To repeat, we use the matrix multiplication operator (@). So
3 @ four
is istr('444')
And
four @ 3
is also istr('444')
It is not allowed to use the
@operator for two istrs. So,four @ fiveraises a TypeError.
Although usually istrs are to be interpreted as an int, that's not a requirement.
So
xxxxxxxxxxistr('abc')
or
xxxxxxxxxxistr('1,2,3')
are perfectly acceptable.
However, we cannot perform any arithmetic or comparison operations with them.
If we try
xxxxxxxxxxistr('abc') + 5
a TypeError will be raised.
That holds for any arithmetic we try.
If we want to test if an istr can be interpreted (and thus used in an arithmetic and comparison expression). we can use the is_int() method. So
ìstr(20).is_int()
is True, whereas
ìstr('abc').is_int()
is False.
The bool operator works normally on the integer value of an istr. So
bool(istr('0')) ==> False
bool(istr('1')) ==> True
But if the istr can't be interpreted as an int, the string value will be used to test. So
bool(istr('abc')) ==> True
bool(istr('')) ==> False
For the in operator, an istr is treated as an ordinary string, although it is possible to use ints as well:
xxxxxxxxxx'34' in istr(1234)34 in istr(1234)
On the left hand side an istr is always treated as a string:
xxxxxxxxxxistr(1234) in '01234566890ABCDEF'
Sorting a list of istrs is based on the integer value, not the string. So
xxxxxxxxxx' '.join(sorted('1 3 2 4 5 6 11 7 9 8 10 12 0'.split()))
is
xxxxxxxxxx'0 1 10 11 2 3 4 5 6 7 8 9'
,whereas
xxxxxxxxxx' '.join(sorted(istr('1 3 2 4 5 6 11 7 9 8 10 12 0'.split()))
is
xxxxxxxxxx'0 1 2 3 4 5 6 7 8 9 10 11'
Apart from with numeric (to be interpreted as an int) or str, istr can be initialized with several other types:
if a dict (or subtype of dict), the same type dict will be returned with all values istr'ed
xxxxxxxxxxistr({'one': 1, 'two':2}) ==> {'one': istr('1'), 'two': istr('2')}
if an iterator, the iterator will be mapped with istr
xxxxxxxxxxmapped = (i for i in istr((i for i in range(2))))print(mapped)print(list(mapped))
this wil print something like
xxxxxxxxxx<generator object <genexpr> at 0x000002A10DE569B0>[istr('0'), istr('1')]
if an iterable, the same type will be returned with all elements istr'ed
xxxxxxxxxxistr([0, 1, 4]) ==> [istr('0'), istr('1'), istr('4')]istr((0, 1, 4)) ==> (istr('0'), istr('1'), istr('4'))istr({0, 1, 4}) ==> `{istr('4'), istr('0'), istr('1')} # or similar
if a range, an istr.range instance will be returned
xxxxxxxxxxistr(range(3)) ==> istr.range(3)list(istr(range(3))) ==> [istr('0'), istr('1'), istr('2')]len(istr(range(3))) ==> 3
if an istr.range instance, the same istr.range will be returned
xxxxxxxxxxistr(istr.range(5)) ==> istr.range(5)
if an istr, the same istr will be returned
xxxxxxxxxxistr(istr('4')) ==> istr ('4')
It is possible to give more than one parameter, in which case a tuple of the istrs of the parameters will be returned, which can be handy to unpack multiple values, e.g.
xxxxxxxxxxa, b, c = istr(5, 6, 7) ==> a=istr('5') , b=istr('6'), c=istr('7')a, b, c = istr(*range(3)) ==> a=istr('0') , b=istr('1'), c=istr('2')
It is possible to test for even/odd (provided the istr can be interpreted as an int) with the is_even and is_odd method, e.g.
xxxxxxxxxxistr(4).is_even()) ==> Trueistr(5).is_odd()) ==> True
It is also possible to test for even/odd of an ordinary int:
xxxxxxxxxxistr.is_even(4) ==> Trueistr.is_odd(5) ==> True
It is possible to test whether an istr is divisible by a given value with the is_divisible_by method, e.g.
xxxxxxxxxxistr(18).is_divisible_by(3) ==> Trueistr(18).is_divisible_by(istr(3)) ==> Trueistr(19).is_divisible_by(3) ==> Falseistr(19).is_divisible_by(istr(3)) == False
It is also possible to test for divisibility of an ordinary int:
xxxxxxxxxxistr.is_divisible(18, 3) ==> Trueistr.is_divisible(19, 3) ==> False
The method divided_by not only tests divisibility, but also returns the result of the division. If not possible, None will be returned,
unless the fallback (last argument) is given, in which case fallback will be returned.
xxxxxxxxxxistr(18).divided_by(3) ==> 6 (actually istr("6"))istr(18).divided_by(istr(3)) ==> 6istr(19).divided_by(3) ==> Noneistr(19).divided_by(3, 0) ==>istr(19).divided_by(3) ==> Noneistr(19).divided_by(istr(3)) ==> Noneistr.divided_by(18, 3) ==> 6istr.divided_by(19, 3) ==> Noneistr.divided_by(19, 3, 0) ==> 0
It is possible to test whether the value is a perfect square (provided the istr can be interpreted as an int) with the is_square method, e.g.
xxxxxxxxxxistr(4).is_square() ==> Trueistr(5).is_square()) ==> False
It is also possible to test for square of an ordinary int:
xxxxxxxxxxistr.is_square(4) ==> Trueistr.is_square(5) ==> False
It is possible to test whether the value is a perfect cube (provided the istr can be interpreted as an int) with the is_cube method, e.g.
xxxxxxxxxxistr(27).is_cube() ==> Trueistr(28).is_cube()) ==> False
It is also possible to test for cube of an ordinary int:
xxxxxxxxxxistr.is_cube(27) ==> Trueistr.is_cube(28 ==> False
It is possible to test whether the value is a perfect power of a given exponent (provided the istr can be interpreted as an int) with the is_power_of method, e.g.
xxxxxxxxxxistr(81).is_power_of(4) ==> Trueistr(82).is_power_of(4) ==> False
It is also possible to test for power of of an ordinary int:
xxxxxxxxxxistr.is_power_of(81, 4) ==> Trueistr.is_power_of(82, 4) ==> False
It is possible to test whether the value is a prime number (provided the istr can be interpreted as an int) with the is_prime method, e.g.
xxxxxxxxxxistr(4).is_prime() ==> Falseistr(5).is_prime()) ==> True
It is also possible to test for prime of an ordinary int:
xxxxxxxxxxistr.is_prime(4) ==> Falseistr.is_prime(5) ==> True
With the all_distinct method, it is possible to test whether all characters are distinct (i.e. no character appears more than once).
xxxxxxxxxxistr('01234').all_distict() ==> Trueistr('012340').all_distict() ==> Falsen98 = istr(98)n100 = n98 + 2istr(n98).all_distinct() ==> Trueistr(n100).all_distinct() ==> False
With the is_consecutive method, it is possible to test whether the individual digits (characters) are consecutive. ASCII-ordering is applied.
xxxxxxxxxxistr('123').is_consecutive() ==> Trueistr('124').is_consecutive() ==> False
Note that this method can also be used for non-istr-s, like istr.is_consecutive(123) ==> True
With the is_triangular method, it is possible to test whether this is a triangular number (sum of integers, starting at 1):
xxxxxxxxxxistr(6).is_triangular() ==> Trueistr(7).is_triangular() ==> False
Note that this method can also be used for non-istr-s, like istr.is_triangular(6) ==> True.
The method reversed() will return an istr with the reversed content:
xxxxxxxxxxistr(456).reversed() ==> istr('654')istr('0456').reversed() ==> istr('6540')
The same can, of course, be achieved with
xxxxxxxxxxistr(456)[::-1] ==> istr('654')istr('0456')[::-1] ==> istr('6540')
It is possible to reverse a negative istr, but the result can't be interpreted as an int anymore.
xxxxxxxxxxistr(-456).reversed() + 3 ==> TypeError
The istr.enumerate class method can be used just as the built-in enumerate function.
The iteration counter however is an istr rather than an int. E.g.
xxxxxxxxxxfor i, c in istr.enumerate('abc'):print(f'{repr(i)} {c}')
prints
xxxxxxxxxxistr('0') aistr('1') bistr('2') c
istr.join can be used just like str.join. The result will be an istr.
On top of that, istr.join may be used as a class method, like
istr.join(("1", "2", "3")) ==> istr("123") ("" is applied as separator)
istr.join("0", ("1", "2", "3")) ==> istr("10203"))
All methods in itertools are also available directly from istr. Note that the result is istr-ed (apart from groupby and tee).
The following class methods are supported (provided their counterpart exists in the installed Python version's itertools):
istr.accumulate
istr.chain
istr.combinations
istr.combinations_with_replacement
istr.compress
istr.count
istr.cycle
istr.dropwhile
istr.filterfalse
istr.groupby (not istr-ed)
istr.islice
istr.pairwise
istr.permutations
istr.product
istr.repeat
istr.starmap
istr.takewhile
istr.tee (not istr-ed)
istr.zip_longest
This can be handy as these methods don't have to be imported from itertools anymore.
All methods have exactly the same (optional) parameters as their itertools counterpart.
For example:
xxxxxxxxxxlist(istr.repeat(1, 4)) ==> [istr('1'), istr('1'), istr('1'), istr('1')]next(istr.count(3)) ==> istr('3')
One more example:
xxxxxxxxxxfor t in istr.permutations(range(3)):print(t)
results in
xxxxxxxxxx(istr('0'), istr('1'), istr('2'))(istr('0'), istr('2'), istr('1'))(istr('1'), istr('0'), istr('2'))(istr('1'), istr('2'), istr('0'))(istr('2'), istr('0'), istr('1'))(istr('2'), istr('1'), istr('0'))
The istr.concat method can be useful to map all items of an iterable
to istr and then concatenate these.
`
xxxxxxxxxxlist(istr.concat(((1,2),(3,4))) ==> istr([12,34])list(istr.concat(istr.permutations(range(3),2))) ==>[istr('01'), istr('02'), istr('10'), istr('12'), istr('20'), istr('21')]
The method prod can be used to return the product of an iterable (including an istr), like math.prod, but as istr.
Thus, istr.prod(range(1,5)) is istr(24)
And istr("123", start=4) is also istr(24).
It is also possible to apply prod on an istr:
istr(1234).prod() is istr(24)
istr("123").prod(start=4) is istr(24)
The class method istr.sumprod(), is equivalent to math.sumprod(), but applies istr to both iterables.
Note that this method is available even in Python < 3.12 .
Thus, istr.sumprod("12", (3,4)) is istr(11)
In contrast to math.sumprod(), istr.sumprod() supports a strict parameter (True by default)
Thus, istr.sumprod("12", (3,4,5), strict=False) is istr(11), whereas istr.sumprod("12", (3,4,5))
raises a ValueError.
The class methods istr.squares, istr.cubes, istr.power_ofs and istr.primes can be used to get a list of all squares, cubes, power_ofs or primes up to a given upperbound (non inclusive) or between a given lower bound and upper bound (non inclusive), like:
istr.squares (100) returns a list of all squares <100
istr.squares(50, 100) returns a list of all squares in the range [50, 100)
Unless cache=False is specified, the query result is cached.
The class method digits can be used to return an istr of digits according to a given specification.
The method takes either no or a number of arguments.
If no arguments are given, the result will be istr('0123456789').
The given argument(s) result in a range of digits.
<n> ==> n
<n-m> ==> n, n+1, ..., m
-n> ==> 0, 1, ... n
n-> ==> n, n+1, ..., 9 if n is numeric (0-9), n, n+1, ... Z if n is a letter
'-' ==> 0, 1, ..., 9
'' ==> 0, 1, ..., 9
(n and m must be digits between 0 and 9 or letters letters between A and Z)
When no stop value is specified, it will be
9 if the start value is between 0 and 9
Z if the start value is between A and Z
The final result is an istr composed of the given range(s).
Here are some examples:
xxxxxxxxxxistr.digits() ==> istr('0123456789')istr.digits('') ==> istr('0123456789')istr.digits('1') ==> istr('1')istr.digits('3-') ==> istr('3456789')istr.digits('-3') ==> istr('0123')istr('1-4', '6', '8-9') ==> istr('1234689')istr('1', '1-2', '1-3') ==> istr('11213')istr.digits('-z') ==> istr('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ')istr.digits('A-F') ==> istr('ABCDEF')istr.digits('C') ==> istr('C')istr.digits('3-') ==> istr('34567879')istr.digits('X-') ==> istr('XYZ')
When we have an istr, we can decompose the value into individual one letter (global) variables with the decompose() method.
E.g.
xxxxxxxxxxistr(485).decompose("abc")
will set the global variables a, b and c to be set to istr(4). istr(8) andistr(5).
Note that the length of the letters specifier must be the same as the length of the istr. Furthermore, multiple values for the same variables result in a ValueError.
To decompose an istr into individual variables, it is arguably easier and safer to unpack the istr, like
xxxxxxxxxxa, b, c = istr(485)
With istr.compose(), an istr can be constructed from individual (global) variables and digits.
E.g.
xxxxxxxxxxx = 3y = 9z = 6test1 = istr.compose("xyz")test2 = istr.compose("xyz0")
Now, test1 will be istr(396) and test2 will be istr(3960).
Composing can also be done by prefixing a string with '=', like:
xxxxxxxxxxtest1 = istr("=xyz")test2 = istr("=xyz0")Now, `test1` will be `istr(396)` and `test2` will be `istr(3960)`.
Note that str(istr("=")) is "=".
Composing and assignment can be done by prefixing a string with ':=', like:
xxxxxxxxxxif istr(":=xyz") > 300:print(f"{xyz=}") # ==> will print xyz=396
Note that str(istr(":=")) is ":=" and does not assign any value.
Usually, composing and decomposing uses the globals namespace, but this can be overridden with the namespace parameter. See the test suite for details.
When a class is derived from istr, all methods will return that newly derived class.
E.g.
xxxxxxxxxxclass jstr(istr.type):...print(repr(jstr(4) * jstr(5)))
will print jstr('20')
It is possible to control the way an istr instance will be repr'ed.
By default, istr(5) is represented as istr('5').
With the istr.repr_mode() context manager, that can be changed:
xxxxxxxxxxwith istr.repr_mode('str'):five = istr(5)print(repr(five))with istr.repr_mode('int'):five = istr(5)print(repr(five))with istr.repr_mode('istr'):five = istr(5)print(repr(five))
This will print
xxxxxxxxxx'5'5istr('5')
If the repr_mode is 'int' and the istr can't be interpreted as an int the string ? will be returned:
xxxxxxxxxxwith istr.repr_mode('int'):abc = istr('abc')print(repr(abc))
This will print
xxxxxxxxxx?
The way an
istris represented is determined at initialization.
It is also possible to set the repr mode without a context manager:
xxxxxxxxxxistr.repr_mode('str')five = istr('5')print(repr(five))
This will print
xxxxxxxxxx'5'
Finally, the current repr mode can be queried with istr.repr_mode(). So upon start:
xxxxxxxxxxprint(repr(istr.repr_mode()))
will output istr.
By default, istr works in base 10. However it is possible to change the base system with the istr.base() context manager / method.
Any base between 2 and 36 may be used.
Note that the integer is always stored in base 10 mode, but the string representation will reflect the chosen base (at time of initialization).
Some examples:
xxxxxxxxxxwith istr.base(16):a = istr('7fff')print(int(a))b = istr(127)print(repr(b))
This will result in
xxxxxxxxxx32767istr('7F')
All calculations are done in the decimal 10 base system.
Note that the way an istr is interpreted is determined at initialization.
It is also possible to set the repr mode without a context manager:
xxxxxxxxxxistr.base(16)print(int(istr('7fff')))
This will print
xxxxxxxxxx32767
Finally, the current base can be queried with istr.base(), so upon start:
xxxxxxxxxxprint(istr.base())
will result in 10.
When an istr is initialized with a string the istr will be always stored as such.
xxxxxxxxxxrepr('4')) ==> istr('4')repr(' 4')) ==> istr(' 4')repr('4 ')) ==> istr('4 ')
For initializing with an int (or other numeric) value, the string is by default simply the str representation
xxxxxxxxxxrepr(4)) ==> istr('4')
With the istr.int_format() context manager this behavior can be changed.
If the format specifier is a number, most likely a single digit, that
will be the minimum number of characters in the string:
xxxxxxxxxxwith istr.int_format('3'):print(repr(istr(1)))print(repr(istr(12)))print(repr(istr(123)))print(repr(istr(1234)))
will print
xxxxxxxxxxistr(' 1')istr(' 12')istr('123')istr('1234')
If the string starts with a 0, the string will be zero filled:
xxxxxxxxxxwith istr.int_format('03'):print(repr(istr(1)))print(repr(istr(12)))print(repr(istr(123)))print(repr(istr(1234)))
will print
xxxxxxxxxxistr('001')istr('012')istr('123')istr('1234')
For bases other than 10, the string will never be reformatted!
The table below indicates whether the string or integer version of istr is applied.
xxxxxxxxxxoperator/function int str Example-----------------------------------------------------------------------------------------+ x istr(20) + 3 ==> istr('23')_ x istr(20) - 3 ==> istr('17')* x istr(20) * 3 ==> istr('60')/ x istr(20) / 3 ==> istr('6')// x istr(20) // 3 ==> istr('6')% x istr(20) % 3 ==> istr('2')divmod x divmod(istr(20), 3) ==> (istr('6'), istr('2'))** x istr(2) ** 3 ==> istr('8')<=, <, >, >= x istr('100') > istr('2') ==> Trueabs x abs(istr(-20)) ==> istr('20')int x int(istr("20")) ==> 20float x float(istr("20")) ==> 20.0complex x complex(istr("20")) ==> (20+0j)== x x istr(20) == 20 ==> True | istr(20) == '20' ==> Truebool x x *) bool(istr(' 0 ')) ==> False | bool(istr('')) ==> False@ x istr(20) @ 3 ==> istr('202020')| x istr(20) | '5' ==> istr('205')slicing x istr(12345)[1:3] ==> istr('23')iterate x [x for x in istr(20)] ==> [istr('2'), istr('0')]len x len(istr(' 20 ')) ==> 4count x istr(100).count('0') ==> 2index x istr(' 100 ').index('0') ==> 2split x istr('1 2').split() ==> (istr('1'), istr('2'))string format x f"|{istr(1234):6}|" ==> '|1234 |'other string methods x istr('aAbBcC').lower() ==> istr('aabbcc')istr('aAbBcC').islower() ==> Falseistr(' abc ').strip() ==> istr('abc')...-----------------------------------------------------------------------------------------*) str is applied if is_int() is False
There's an extensive pytest script in the \tests directory.
This script also shows clearly the ways istr can be used, including several edge cases. Highly recommended to have a look at.
You can contact Ruud van der Ham, the core developer, via ruud@salabim.org .