"""
Tools for NewBase60, NewBase64 and NewDateTime numbering systems.
NewBase60
---------
NewBase60 is a base 60 (sexagesimal) numbering system that uses only
ASCII numbers and letters that are print, prose and code safe. Lowercase
l and uppercase I are aliased to the number 1. Capital O is aliased to
the number 0.
0123456789ABCDEFGHJKLMNPQRSTUVWXYZ_abcdefghijkmnopqrstuvwxyz
NewBase60 encoded numbers sort properly in the typical case.
NewBase64
---------
NewBase64 is a base 64 numbering system that extends NewBase60 for use
in shortly encoding square grids. The four characters added are -, +, *
and $.
0 1 2 3 4 5 6 7
----------------
0| 0 1 2 3 4 5 6 7
8| 8 9 A B C D E F
16| G H J K L M N P
24| Q R S T U V W X
32| Y Z _ a b c d e
40| f g h i j k m n
48| o p q r s t u v
56| w x y z - + * $
latitude: -90 to 90 (pole to pole)
longitude: -180 to 180 (meridian back to meridian)
Earth is split into 32 quadrants:
0 1 2 3 4 5 6 7
8 9 A B C D E F
G H J K L M N P
Q R S T U V W X
Each quadrant can be further scoped by 1/64ths. e.g. the 0th quadrant:
0 1 2 3 4 5 6 7
8 9 A B C D E F
G H J K L M N P
Q R S T U V W X
Y Z _ a b c d e
f g h i j k m n
o p q r s t u v
w x y z - + * $
The initial quadrant is divided into 64 subquadrants with dimensions
5.625 degrees square. Second and third subquadrants are 0.703125 and
0.087890625 degrees square respectively.
1 degree latitude = between 110.567 (at equator) and 111.699 km (at poles)
1 degree longitude = between 111.321 (at equator) and 0 (at poles)
precision scope
1 4.950 Mm
2 618.75 km
3 77.34375 km
4 9.66796875 km
5 1.20849609375 km
6 151.06201171 m
7 18.8827514648 m
8 2.36034393311 m
9 295.04299163 mm
10 36.88037395 mm
11 4.61004674 mm
12 576.25584 μm
13 72.0319803804 μm
14 9.00399754755 μm
15 1.12549969344 μm
NewDateTime
-----------
Four sundays (0th day of the week) mark solar equinoxes and solstices.
Monday through Saturday are days 1-6. Friday and Saturday are weekends
resulting in a "4 day work week".
5 days * 52 weeks = 260 work days in existing calendar
5 days * 50 weeks = 250 work days accounting for two weeks vacation
4 days * 60 weeks = 240 work days in NewDateTime
Four 90 day, 15 week quarters. Each quarter is split into three months.
52 days * 4 quarters = 184 work days accounting for mid-quarter vacation
Apr May Jun Jul Aug Sep Oct Nov Dec Jan Feb Mar
0123456789ABCDEFGHJKLMNPQRSTUVWXYZ_abcdefghijkmnopqrstuvwxyz
Su @--------------@--------------@--------------@-------------- 0
Mo X X X X 1
Tu X X X X 2
We X X X X X X X X X X X X 3
Th X X X X X X X X X X X X 4
Fr XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 5
Sa XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 6
@ - sunday
X - non-work
0 year 1
000 new epoch
000 Sunday
000 northern tilt - new first sunday (old Mar 20, equinox)
0F0 northern sun - new second sunday (old Jun 21, northern solstice)
0W0 southern tilt - new third sunday (old Sep 23, equinox)
0k0 southern sun - new fourth sunday (old Dec 21, southern solstice)
001 Monday
002 Tuesday
003 Wednesday
004 Thursday
005 Friday
006 Saturday
0z6 last day of first year
100 second new new year
1 year 2
1R 26th week of the 2nd year
1R0 Monday of the 26th week of the 2nd year
Based upon @[Tantek Çelik][1]'s [NewBase60][2], [NewBase64][3] and
[NewCalendar][4] specifications.
[1]: https://tantek.com
[2]: https://tantek.pbworks.com/NewBase60
[3]: https://tantek.pbworks.com/NewBase64
[4]: https://tantek.pbworks.com/NewCalendar
"""
import pendulum
from Crypto.Random import random
nb60 = "0123456789ABCDEFGHJKLMNPQRSTUVWXYZ_abcdefghijkmnopqrstuvwxyz"
nb60_re = "[0-9A-HJ-NP-Z_a-km-z]"
_nb60map = dict(zip(nb60, range(len(nb60))))
_nb60map["l"] = 1 # typo l to 1
_nb60map["I"] = 1 # typo I to 1
_nb60map["O"] = 0 # typo O to 0
def nbencode(number, min_length=1):
"""
return the NewBase60 string equivalent of decimal integer `i`
`min_length`: final size of string, zero padding on the left if needed
>>> nbencode(2011)
'ZX'
>>> nbencode(2011, min_length=5)
'000ZX'
>>> nbencode(-2011)
'-ZX'
"""
string = ""
try:
number = int(number)
except ValueError:
raise ValueError("NewBase60 encoding requires a decimal integer")
sign = ""
if number < 0:
sign = "-"
number = abs(number)
while number > 0:
number, i = divmod(number, 60)
string = nb60[i] + string
length = len(string)
min_length = int(min_length)
if length < min_length:
string = "0" * (min_length - length) + string
return sign + string
def nbdecode(string):
"""
return the decimal integer equivalent of NewBase60 string `s`
>>> nbdecode('ZX')
2011
>>> nbdecode('000ZX')
2011
>>> nbdecode('-ZX')
-2011
"""
string = str(string)
sign = 1
if string.startswith("-"):
sign = -1
string = string[1:]
number = 0
for character in string:
number = number * 60 + _nb60map.get(character, 0)
return number * sign
def nbrandom(length):
"""
return a random NewBase60 string `length` digits long
>>> nbrandom(3) # doctest: +SKIP
'Do_'
>>> nbrandom(6) # doctest: +SKIP
'j9cQ4m'
"""
start = nbdecode("1" + "0" * (length - 1))
end = nbdecode("z" * length)
return nbencode(random.randint(start, end))
def nbrange(start, stop=None, step=1):
"""
return a generator that yields NewBase60 strings like the builtin `range`
Arguments will be coerced to their NewBase60 string representations if
not provided as such.
>>> list(nbrange(5))
['0', '1', '2', '3', '4']
>>> list(nbrange('ex', 'f3'))
['ex', 'ey', 'ez', 'f0', 'f1', 'f2']
"""
if stop is None:
stop = start
start = "0"
for i in range(nbdecode(start), nbdecode(stop), step):
yield nbencode(i)
def ncencode(dt):
""" """
ddd = int(dt.format("DDD"))
bimnumber = int((ddd - 1) / 61) + 1
bim = hex(bimnumber + 9)[2].upper()
dayinbim = int((ddd - 1) % 61) + 1
seconds = round((dt.hour * 3600) + (dt.minute * 60) + dt.second) / 24
return nbencode(dt.year), bim, nbencode(dayinbim), nbencode(seconds)
def ncdecode(year, bim, dayinbim, seconds):
""" """
bimnumber = int("0x" + bim, 16) - 9
ddd = 61 * (bimnumber - 1) + dayinbim
return pendulum.parse("{}-{:03}".format(year, ddd))