tablinum 0.1.0__py3-none-any.whl

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.
tablinum/__about__.py ADDED
@@ -0,0 +1,4 @@
1
+ # SPDX-FileCopyrightText: 2024-present Toby Thurston <toby.thurston@gmail.com>
2
+ #
3
+ # SPDX-License-Identifier: MIT
4
+ __version__ = "0.1.0"
tablinum/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ # SPDX-FileCopyrightText: 2024-present Toby Thurston <toby.thurston@gmail.com>
2
+ #
3
+ # SPDX-License-Identifier: MIT
4
+ from tablinum.tablinum import Table as Table
@@ -0,0 +1,301 @@
1
+ #! /usr/bin/env python3
2
+ '''Date functions to use in tabulate `arr` expressions
3
+
4
+ Toby Thurston -- 10 Feb 2021
5
+
6
+ "dow": tab_fun_dates.dow,
7
+ "base": tab_fun_dates.base,
8
+ "date": tab_fun_dates.date,
9
+
10
+ "hms": tab_fun_dates.hms,
11
+ "hr": tab_fun_dates.hr,
12
+ "mins": tab_fun_dates.mins,
13
+ "secs": tab_fun_dates.secs,
14
+
15
+ '''
16
+ import datetime
17
+
18
+
19
+ def parse_date(sss, today=None):
20
+ '''Try to parse a date
21
+ >>> parse_date(730486).isoformat()
22
+ '2001-01-01'
23
+ >>> parse_date("1 January 2001").isoformat()
24
+ '2001-01-01'
25
+ >>> parse_date(base("1 January 2001")).isoformat()
26
+ '2001-01-01'
27
+ >>> parse_date("6/7/95").isoformat()
28
+ '1995-07-06'
29
+ >>> parse_date("2022-W47-2").isoformat()
30
+ '2022-11-22'
31
+ >>> parse_date("Fri 1st Apr 2022").isoformat()
32
+ '2022-04-01'
33
+ >>> parse_date("Sat 29th July 2023").isoformat()
34
+ '2023-07-29'
35
+ >>> parse_date("Fri 29th Sept 2023").isoformat()
36
+ '2023-09-29'
37
+ >>> parse_date("Sat 19th Mar 2022").isoformat()
38
+ '2022-03-19'
39
+ >>> parse_date("Sunday", today='2022-03-17').isoformat()
40
+ '2022-03-20'
41
+ '''
42
+ try:
43
+ if 0 < int(sss) < 900000:
44
+ return datetime.date.fromordinal(sss)
45
+ except (TypeError, ValueError) as _:
46
+ pass
47
+
48
+ days = "Monday Tuesday Wednesday Thursday Friday Saturday Sunday".split()
49
+ try:
50
+ iso_dow = 1 + days.index(str(sss).capitalize())
51
+ except ValueError:
52
+ pass
53
+ else:
54
+ if 1 <= iso_dow <= 7:
55
+ if today is None:
56
+ today = datetime.datetime.today()
57
+ else:
58
+ today = datetime.datetime.strptime(today, '%Y-%m-%d')
59
+ year, week = today.strftime("%G-%V").split("-")
60
+ return datetime.datetime.strptime(f'{year}-W{week}-{iso_dow}', "%G-W%V-%u").date()
61
+
62
+ sss = str(sss).replace('Sept ', 'Sep ')
63
+
64
+ for fmt in ('%Y-%m-%d', '%Y%m%d', '%d %B %Y', '%d %b %Y', '%G-W%V-%u', '%d-%b-%Y',
65
+ '%d %b %y', '%d %B %y', '%d/%m/%Y', '%d/%m/%y', '%B %d, %Y',
66
+ '%A %d %B %Y',
67
+ '%a %dth %b %Y', '%a %dst %b %Y', '%a %dnd %b %Y', '%a %drd %b %Y',
68
+ '%a %dth %B %Y', '%a %dst %B %Y', '%a %dnd %B %Y', '%a %drd %B %Y',
69
+ '%m/%d/%Y',
70
+ '%c', '%x'):
71
+ try:
72
+ return datetime.datetime.strptime(str(sss), fmt).date()
73
+ except ValueError:
74
+ pass
75
+
76
+ raise ValueError
77
+
78
+
79
+ def dow(sss, date_format="%a"):
80
+ '''Is it Friday yet?
81
+ >>> dow("1 January 2001")
82
+ 'Mon'
83
+ >>> dow(base("1 January 2001"))
84
+ 'Mon'
85
+ >>> dow("31/12/2021")
86
+ 'Fri'
87
+ >>> dow("1 January 2001", "%A")
88
+ 'Monday'
89
+ >>> dow("date")
90
+ '%a'
91
+
92
+ You can actually use this to produce any strftime format...
93
+
94
+ >>> dow("25 Dec 2001", "%c")
95
+ 'Tue Dec 25 00:00:00 2001'
96
+
97
+ >>> dow("25 Dec 2001", "%u")
98
+ '2'
99
+
100
+ '''
101
+ try:
102
+ return parse_date(sss).strftime(date_format)
103
+ except (TypeError, ValueError):
104
+ return date_format
105
+
106
+
107
+ def base(sss=None):
108
+ '''Get today's date as "base" number, or whatever date you give
109
+ >>> base("1 January 2001")
110
+ 730486
111
+ >>> base("1 January 1901")
112
+ 693961
113
+ >>> base("01/01/01")
114
+ 730486
115
+ >>> base("20010102")
116
+ 730487
117
+ >>> base(20010102)
118
+ 730487
119
+ >>> base("2001-01-03")
120
+ 730488
121
+ >>> base("03-jan-2001")
122
+ 730488
123
+ >>> base("31 Dec 2000")-base("1 Jan 1901")
124
+ 36524
125
+ >>> base() - datetime.date.today().toordinal()
126
+ 0
127
+ >>> datetime.date.today().toordinal() - base(-4)
128
+ 4
129
+ >>> base('Date')
130
+ 'base(Date)'
131
+ '''
132
+ if sss is None:
133
+ return datetime.date.today().toordinal()
134
+
135
+ if isinstance(sss, int) and abs(sss) < 1000:
136
+ return datetime.date.today().toordinal() + sss
137
+
138
+ try:
139
+ return parse_date(sss).toordinal()
140
+ except (TypeError, ValueError):
141
+ return f'base({sss})'
142
+
143
+
144
+ def date(ordinal=0):
145
+ '''Turn a base number (or an epoch time or a millisecond epoch time) into a date
146
+ >>> date(716257)
147
+ '1962-01-17'
148
+ >>> date(3652059)
149
+ '9999-12-31'
150
+ >>> date(3652060)
151
+ '1970-02-12T07:27:40'
152
+ >>> date(100000000000)
153
+ '5138-11-16T09:46:40'
154
+ >>> date(100000000001)
155
+ '1973-03-03T09:46:40.001000'
156
+ >>> date(10) == (datetime.date.today() + datetime.timedelta(days=10)).isoformat()
157
+ True
158
+ >>> date('ts')
159
+ 'date(ts)'
160
+ >>> date(-10000000000000000000000000) == date(0)
161
+ True
162
+ >>> date(-2000) == date(0)
163
+ True
164
+ >>> date(base(730486))
165
+ '2001-01-01'
166
+ '''
167
+ try:
168
+ ordinal = int(ordinal)
169
+ except (TypeError, ValueError):
170
+ ordinal = base(ordinal)
171
+ if ordinal.startswith('base'):
172
+ return ordinal.replace('base', 'date')
173
+
174
+ if abs(ordinal) < 1000:
175
+ dt = datetime.date.today() + datetime.timedelta(days=ordinal)
176
+ elif ordinal > 100000000000: # about 5000 AD as an epoch, so assume epoch ms
177
+ dt = datetime.datetime.fromtimestamp(ordinal / 1000)
178
+ elif ordinal > datetime.date.max.toordinal(): # > max date, so assume epoch seconds
179
+ dt = datetime.datetime.fromtimestamp(ordinal)
180
+ else:
181
+ try:
182
+ dt = datetime.date.fromordinal(ordinal)
183
+ except (TypeError, ValueError, OverflowError):
184
+ dt = datetime.date.today()
185
+
186
+ return dt.isoformat()
187
+
188
+
189
+ def hms(fractional_things):
190
+ '''Turn decimal hours/degrees into h/d:mm:ss.fff
191
+
192
+ >>> hms(57.2957795)
193
+ '57:17:44.806'
194
+
195
+ >>> hms(10801/3600)
196
+ '3:00:01.000'
197
+ '''
198
+ hh, r = divmod(fractional_things, 1)
199
+ mm, r = divmod(r * 60, 1)
200
+ return "{}:{:02d}:{:06.3f}".format(int(hh), int(mm), r * 60)
201
+
202
+
203
+ def hr(hms_word, s=0):
204
+ '''Turn hh:mm:ss.sss into fractional hours
205
+
206
+ >>> hr("12:34:56")
207
+ 12.582222222222223
208
+ '''
209
+ hours = 0
210
+ for i, p in enumerate(hms_word.split(':')):
211
+ hours += float(p) * 60 ** (s - i)
212
+ return hours
213
+
214
+
215
+ def mins(hms_word):
216
+ '''Turn hh:mm:ss.sss into fractional minutes
217
+ >>> mins('1:23:45')
218
+ 83.75
219
+ '''
220
+ return hr(hms_word, 1)
221
+
222
+
223
+ def secs(hms_word):
224
+ '''Turn hh:mm:ss.sss into fractional seconds
225
+ >>> secs('1:23:45.67')
226
+ 5025.67
227
+ '''
228
+ return hr(hms_word, 2)
229
+
230
+
231
+ def as_time(time_string):
232
+ '''Turn a possible time into hh:mm
233
+
234
+ >>> a = ['12 midnight', '12.00 a.m.', '12:01 am', '1:00a.m.', '9am', '11.00am', '11:59 a.m.']
235
+ >>> a += ['Noon', '12 noon', '12:00 pm', '12:01 p.m.', '1:00pm', '4 p.m.', '11:00p.m.', '11:59 pm', '12:00']
236
+ >>> ' '.join(as_time(x) for x in a)
237
+ '00:00 00:00 00:01 01:00 09:00 11:00 11:59 12:00 12:00 12:00 12:01 13:00 16:00 23:00 23:59 12:00'
238
+ '''
239
+ ts = ''.join(time_string.lower().split()).replace('.', '').replace(':', '')
240
+ if ts in ('noon', '12noon', '12pm', '1200pm'):
241
+ return '12:00'
242
+ if ts in ('midnight', '12midnight', '12am', '1200am'):
243
+ return '00:00'
244
+ if ts.endswith('am'):
245
+ if len(ts) <= 4:
246
+ hh = int(ts[:-2]) % 12
247
+ mm = 0
248
+ else:
249
+ hh = int(ts[:-4]) % 12
250
+ mm = int(ts[-4:-2])
251
+ return f'{hh:02d}:{mm:02d}'
252
+ if ts.endswith('pm'):
253
+ if len(ts) <= 4:
254
+ hh = int(ts[:-2]) % 12 + 12
255
+ mm = 0
256
+ else:
257
+ hh = int(ts[:-4]) % 12 + 12
258
+ mm = int(ts[-4:-2])
259
+ return f'{hh:02d}:{mm:02d}'
260
+
261
+ return time_string
262
+
263
+
264
+ def epoch(date_time_string):
265
+ '''Turn date time into epoch secs
266
+
267
+ >>> epoch("01/01/1970 00:00")
268
+ '0'
269
+ >>> epoch("2000-12-31 23:59:59")
270
+ '978307199'
271
+ >>> epoch("Thursday")
272
+ 'Thursday'
273
+ '''
274
+ for fd in ('%Y-%m-%d', '%d/%m/%Y', '%d-%b-%Y', '%Y%m%d'):
275
+ for ft in ('%H:%M:%S', '%H:%M', '%H%M%S'):
276
+ try:
277
+ dt = datetime.datetime.strptime(date_time_string, f'{fd} {ft}').replace(tzinfo=datetime.timezone.utc)
278
+ except ValueError:
279
+ pass
280
+ else:
281
+ return f'{dt.timestamp():.0f}'
282
+ return date_time_string
283
+
284
+
285
+ def UK_tax_year(sss):
286
+ '''Return the UK tax year
287
+
288
+ >>> UK_tax_year('1962-01-17')
289
+ 'FY61/62'
290
+ >>> UK_tax_year('2023-09-17')
291
+ 'FY23/24'
292
+
293
+ '''
294
+ b = base(sss)
295
+ year = int(date(b)[:4])
296
+ c = base(f'{year}-04-05')
297
+ if c > b:
298
+ year -= 1
299
+
300
+ return f'FY{year % 100}/{(year+1) % 100}'
301
+
@@ -0,0 +1,266 @@
1
+ '''Mathematical functions for tabulate's decimal numbers
2
+ '''
3
+ import decimal
4
+ import itertools
5
+ import math
6
+ import re
7
+
8
+ # pylint: disable=C0103
9
+
10
+ decimal.getcontext().prec = 12
11
+
12
+ PIC = ''.join('''3.1415926535 8979323846 2643383279 5028841971
13
+ 6939937510 5820974944 5923078164 0628620899 8628034825 3421170679'''.split())
14
+ PI = 0 + decimal.Decimal(PIC)
15
+ TAU = PI + PI
16
+ ONE = decimal.Decimal('1')
17
+ ZERO = decimal.Decimal('0')
18
+ one = decimal.Decimal('1.0000000000')
19
+
20
+
21
+ def degrees(x):
22
+ '''return radians -> degrees
23
+ >>> degrees(1)
24
+ Decimal('57.2957795131')
25
+ >>> degrees(TAU)
26
+ Decimal('360')
27
+ >>> degrees(0)
28
+ Decimal('0')
29
+ '''
30
+ if not x:
31
+ return ZERO
32
+ return x / PI * 180
33
+
34
+
35
+ def radians(x):
36
+ ''' return degrees -> radians
37
+ >>> radians(90)
38
+ Decimal('1.57079632679')
39
+ >>> radians(0)
40
+ Decimal('0')
41
+ '''
42
+ if not x:
43
+ return ZERO
44
+ return x * PI / 180
45
+
46
+
47
+ def sin(x):
48
+ '''sine
49
+
50
+ >>> sin(0)
51
+ Decimal('0')
52
+ >>> sin(PI/6)
53
+ Decimal('0.5')
54
+ >>> sin(PI/2)
55
+ Decimal('1')
56
+ '''
57
+ return decimal.Decimal(math.sin(x)).quantize(one).normalize()
58
+
59
+
60
+ def cos(x):
61
+ '''cosine
62
+
63
+ >>> cos(PI/2)
64
+ Decimal('-0')
65
+ >>> cos(PI/3)
66
+ Decimal('0.5')
67
+ >>> cos(0)
68
+ Decimal('1')
69
+ '''
70
+ return decimal.Decimal(math.cos(x)).quantize(one).normalize()
71
+
72
+
73
+ def tan(x):
74
+ '''tangent
75
+ >>> tan(PI/4)
76
+ Decimal('1')
77
+ '''
78
+ return decimal.Decimal(math.tan(x)).quantize(one).normalize()
79
+
80
+
81
+ def sind(x):
82
+ '''sin in degrees
83
+
84
+ >>> sind(0)
85
+ Decimal('0')
86
+ >>> sind(30)
87
+ Decimal('0.5')
88
+ >>> sind(90)
89
+ Decimal('1')
90
+ '''
91
+ return decimal.Decimal(math.sin(math.radians(x))).quantize(one).normalize()
92
+
93
+
94
+ def cosd(x):
95
+ '''cosine in degrees
96
+
97
+ >>> cosd(90)
98
+ Decimal('0')
99
+ >>> cosd(60)
100
+ Decimal('0.5')
101
+ >>> cosd(0)
102
+ Decimal('1')
103
+ '''
104
+ return decimal.Decimal(math.cos(math.radians(x))).quantize(one).normalize()
105
+
106
+
107
+ def tand(x):
108
+ '''tan in degrees
109
+ >>> tand(90)
110
+ Decimal('Infinity')
111
+ >>> tand(45)
112
+ Decimal('1')
113
+ >>> tand(0)
114
+ Decimal('0')
115
+ '''
116
+ if x % 360 in (90, 270):
117
+ return decimal.Decimal('Inf')
118
+
119
+ return decimal.Decimal(math.tan(math.radians(x))).quantize(one).normalize()
120
+
121
+
122
+ def pyth_add(a, b):
123
+ '''Pythagorean addition
124
+
125
+ >>> pyth_add(5, 12)
126
+ Decimal('13')
127
+ '''
128
+ return (ONE * a * a + b * b).sqrt()
129
+
130
+
131
+ def angle(a, b):
132
+ '''Like MP
133
+ >>> angle(4,3)
134
+ Decimal('36.8698976462')
135
+ '''
136
+ return degrees(decimal.Decimal(math.atan2(b, a)).quantize(one).normalize())
137
+
138
+
139
+ def dir(t):
140
+ '''from MP
141
+ >>> dir(0)
142
+ (Decimal('1'), Decimal('0'))
143
+ >>> dir(45)
144
+ (Decimal('0.7071067812'), Decimal('0.7071067812'))
145
+ '''
146
+ return (cosd(t), sind(t))
147
+
148
+
149
+ def mexp(x):
150
+ '''from MP = exp(x/256)
151
+ >>> mexp(0)
152
+ Decimal('1')
153
+ >>> mexp(256)
154
+ Decimal('2.71828182846')
155
+ '''
156
+ return (decimal.Decimal(x)/256).exp()
157
+
158
+
159
+ def mlog(x):
160
+ ''' from MP = 256 log(x)
161
+ >>> mlog(10)
162
+ Decimal('589.461783805')
163
+ '''
164
+ return 256 * decimal.Decimal(x).ln()
165
+
166
+
167
+ def decimal_to_hex(d):
168
+ '''decimal to hexadecimal...
169
+
170
+ >>> decimal_to_hex(decimal.Decimal('0'))
171
+ '0x0'
172
+ >>> decimal_to_hex(decimal.Decimal('3.14'))
173
+ '0x3.23d70a3d70'
174
+ >>> decimal_to_hex(decimal.Decimal('100'))
175
+ '0x64'
176
+
177
+ '''
178
+
179
+ digits = '0123456789abcdef'
180
+ i, d = divmod(d, 1)
181
+ a = hex(int(i))
182
+ if d > 0:
183
+ a += '.'
184
+ while d > 0 and len(a) < 2 + decimal.getcontext().prec:
185
+ d *= 16
186
+ i, d = divmod(d, 1)
187
+ a += digits[int(i)]
188
+ return a
189
+
190
+
191
+ def decimal_to_oct(d):
192
+ '''decimal to octal...
193
+
194
+ >>> decimal_to_oct(decimal.Decimal('0'))
195
+ '0o0'
196
+ >>> decimal_to_oct(decimal.Decimal('3.14'))
197
+ '0o3.1075341217'
198
+ >>> decimal_to_oct(decimal.Decimal('100'))
199
+ '0o144'
200
+
201
+ '''
202
+
203
+ digits = '01234567'
204
+ i, d = divmod(d, 1)
205
+ a = oct(int(i))
206
+ if d > 0:
207
+ a += '.'
208
+ while d > 0 and len(a) < 2 + decimal.getcontext().prec:
209
+ d *= 8
210
+ i, d = divmod(d, 1)
211
+ a += digits[int(i)]
212
+ return a
213
+
214
+
215
+ def si(amount):
216
+ """If amount is a number, add largest possible SI suffix,
217
+ otherwise try to remove the suffix and return a value
218
+ >>> si('10M')
219
+ Decimal('10000000')
220
+ >>> si(12315350)
221
+ '12.315 M'
222
+ >>> si(10)
223
+ '10.000'
224
+ >>> si('.2 k')
225
+ Decimal('200.0')
226
+ >>> si('Heading')
227
+ 'Heading'
228
+
229
+ """
230
+ sips = ' kMGTPE'
231
+ m = re.match(rf'([-+]?(?:\d+\.\d*|\.\d+|0|[1-9]\d*))\s*([{sips}])\Z', str(amount))
232
+ if m is not None:
233
+ return decimal.Decimal(m.group(1)) * 10 ** (3 * sips.index(m.group(2)))
234
+ try:
235
+ n = decimal.Decimal(amount)
236
+ except decimal.InvalidOperation:
237
+ return amount
238
+ else:
239
+ e = min(int(n.log10() / 3), len(sips) - 1)
240
+ return '{:7.3f} {}'.format(n / (10 ** (3 * e)), sips[e]).strip()
241
+
242
+
243
+ def ifactors(n):
244
+ "Iterate through the factors"
245
+ f = 2
246
+ f_cycle = itertools.cycle([4, 2, 4, 2, 4, 6, 2, 6])
247
+ increments = itertools.chain([1, 2, 2], f_cycle)
248
+ for incr in increments:
249
+ if f * f > n:
250
+ break
251
+ while n % f == 0:
252
+ yield f
253
+ n //= f
254
+ f += incr
255
+ if n > 1:
256
+ yield n
257
+
258
+
259
+ def factors(n):
260
+ '''find the factors of n and return a list... very slowly
261
+ >>> factors(12345)
262
+ [3, 5, 823]
263
+ >>> factors(128)
264
+ [2, 2, 2, 2, 2, 2, 2]
265
+ '''
266
+ return list(ifactors(n))
@@ -0,0 +1,101 @@
1
+ #! /usr/bin/env python3
2
+
3
+ def t_sorted(*args, reverse=False):
4
+ '''sorted, allowing list of args or a single iterable
5
+ and returning a tuple not a list
6
+
7
+ >>> t_sorted((3,1,2))
8
+ (1, 2, 3)
9
+
10
+ >>> t_sorted(3,4,5,1,0)
11
+ (0, 1, 3, 4, 5)
12
+
13
+ >>> t_sorted(3,4,5,1,0, reverse=True)
14
+ (5, 4, 3, 1, 0)
15
+
16
+ '''
17
+ try:
18
+ it = iter(*args)
19
+ except TypeError:
20
+ return tuple(sorted(args, reverse=reverse))
21
+ else:
22
+ return tuple(sorted(it, reverse=reverse))
23
+
24
+
25
+ def t_apply(*args, fun=all):
26
+ '''Apply fun allowing args to be separate or a tuple
27
+
28
+ >>> t_apply(0,1,0,1)
29
+ False
30
+ >>> t_apply((True, True, True))
31
+ True
32
+ >>> t_apply(0,1,0,1, fun=sum)
33
+ 2
34
+ >>> t_apply((37, 73, 99), fun=sum)
35
+ 209
36
+
37
+ '''
38
+ try:
39
+ it = iter(*args)
40
+ except TypeError:
41
+ return fun(args)
42
+ else:
43
+ return fun(it)
44
+
45
+
46
+ def t_all(*args):
47
+ '''
48
+ >>> t_all(0,1,1)
49
+ False
50
+ '''
51
+ return t_apply(args)
52
+
53
+
54
+ def t_any(*args):
55
+ '''
56
+ >>> t_any(0,1,1)
57
+ True
58
+ '''
59
+ return t_apply(args, fun=any)
60
+
61
+
62
+ def t_min(*args):
63
+ '''
64
+ >>> t_min(0,1,1)
65
+ 0
66
+ '''
67
+ return t_apply(args, fun=min)
68
+
69
+
70
+ def t_minp(*args):
71
+ '''
72
+ >>> t_minp(0,1,5)
73
+ 1
74
+ '''
75
+ return min(x for x in args if x)
76
+
77
+
78
+ def t_max(*args):
79
+ '''
80
+ >>> t_max(0,1,1)
81
+ 1
82
+ '''
83
+ return t_apply(args, fun=max)
84
+
85
+
86
+ def t_sum(*args):
87
+ '''
88
+ >>> t_sum(0,1,1)
89
+ 2
90
+ '''
91
+ return t_apply(args, fun=sum)
92
+
93
+ def length(s):
94
+ '''Length of strings *or* integers
95
+ >>> length("This")
96
+ 4
97
+ >>> length(987613)
98
+ 6
99
+
100
+ '''
101
+ return len(str(s))