forked from ECB2020/Hobyah
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathgenerics.py
655 lines (549 loc) · 22.3 KB
/
generics.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
#! python3
#
# Copyright 2020-2021, Ewan Bennett
#
# All rights reserved.
#
# Released under the BSD 2-clause licence (SPDX identifier: BSD-2-Clause)
#
# email: [email protected]
#
# This file contains a set of general routines used by the other
# programs.
# It issues error messages in the following ranges: 1021-1024.
def PauseFail():
'''We have printed an error message to the runtime screen for
an unexpected failure (typically in the 1000 series error
messages for errors that are not supposed to happen).
Parameters: none
Returns: doesn't return
'''
import sys
if sys.platform == 'win32':
import os
os.system("pause")
# Uncomment the 'raise()' command and comment out the
# sys.exit to get a traceback when running in Terminal.
# raise()
sys.exit()
def Enth(number):
'''Take an integer and return its ordinal as a string (1 > "1st",
11 > "11th", 121 > "121st").
Parameters:
number (int), e.g. 13
Returns:
ordinal (str), e.g. '13th'
Errors:
Aborts with 1021 if not an integer.
Aborts with 1022 if negative.
'''
if type(number) != int:
print('> *Error* type 1021\n'
'> tried to get the ordinal of a non-integer.')
# Abort the run
PauseFail()
elif number < 0:
print('> *Error* type 1022\n'
'> tried to get the ordinal of a negative number.')
# Abort the run
PauseFail()
# Yes, these are pretty lame error messages. They are
# here because they occasionally get triggered during
# development.
# First check for 11th, 12th, 13th, 211th, 212th, 213th etc.
# We have to test this before we test for 1, 2, 3.
if divmod(number,100)[1] in (11, 12, 13):
ordinal = str(number) + 'th'
else:
last_digit = divmod(number,10)[1]
if 1 <= last_digit <= 3:
endings = {1:'st', 2:'nd', 3:'rd'}
ordinal = str(number) + endings[last_digit]
else:
# Every other ordinal (4-9 and 0) ends in 'th'.
ordinal = str(number) + 'th'
return(ordinal)
def Plural(count):
'''Return an empty string if the count is 1, "s" otherwise
'''
if count == 1:
return("")
else:
return("s")
def ColumnText(number):
'''
Take an integer and turn it into its equivalent spreadsheet
column (e.g. 26 > "Z", 27 > "AA" etc). This works for
any positive integer.
Parameters:
number (int), An integer over zero
Returns:
base (str), The equivalent integer in base 26
converted to an A-Z string.
Errors:
Aborts with 1023 if negative or zero.
'''
if number <= 0:
print('> *Error* type 1023\n'
'> tried to get the column letter of an\n'
'> invalid number (' + str(number) + ').')
# Abort the run
PauseFail()
letters = "_ABCDEFGHIJKLMNOPQRSTUVWXYZ"
(quot, rem) = divmod(number, 26)
if rem == 0:
# We are at a Z. Adjust the values
quot -= 1
rem = 26
if quot != 0:
# We need to recurse
val = columnText(quot) + letters[rem]
else:
# We have a value in the range 1 to 26 (we
# never have rem = 0 when we get to here).
val = letters[rem]
return(val)
def ErrorOnLine(line_number, line_text, log, lstrip = True, rstrip = True):
'''Take the line number of a faulty line of input and the line
itself. Write it to to the screen and to the logfile. In a few
circumstances (mostly when complaining about possibly-invalid SES
PRN file header lines) we want to keep the whitespace so those options
are available.
Parameters:
line_number (int), The line number where we failed
line_text (str), The text on the line that failed
log (handle), The handle of the log file
lstrip bool, If True, remove whitespace at the LHS
rstrip bool, If True, remove whitespace at the RHS
Returns: None
'''
if lstrip:
line_text = line_text.lstrip()
if rstrip:
line_text = line_text.rstrip()
message = ('> Faulty line of input (' + Enth(line_number) + ') is\n'
'> ' + line_text)
log.write(message + "\n")
print(message)
return()
def ErrorOnTwoLines(line_number1, line_text1, line_number2, line_text2,
log, lstrip = True, rstrip = True):
'''Take the line numbers of two faulty lines of input and the lines
themselves. Write them to to the screen and to the logfile.
Parameters:
line_number1 (int), The first line number
line_text1 (str), The text on the first line
line_number2 (int), The second line number
line_text2 (str), The text on the second line
log (handle), The handle of the log file
lstrip bool, If True, remove whitespace at the LHS
rstrip bool, If True, remove whitespace at the RHS
Returns: None
'''
if lstrip:
line_text1 = line_text1.lstrip()
line_text2 = line_text2.lstrip()
if rstrip:
line_text1 = line_text1.rstrip()
line_text2 = line_text2.rstrip()
# This is likely to be the definition of a constant followed by
# misuse of the constant, so we'll put the line of entry defining
# the constant first.
message = ('> Faulty lines of input (' + Enth(line_number2)
+ ' and ' + Enth(line_number1) + ') were\n'
'> ' + line_text2 + '\n'
'> ' + line_text1)
log.write(message + "\n")
print(message)
return()
def ErrorOnLine2(line_index, line_triples, log, lstrip = True, rstrip = True):
'''Take the line number of a faulty line of input and the line
triples. Write it to to the screen and to the logfile. In a few
circumstances (mostly when complaining about possibly-invalid SES
PRN file header lines) we want to keep the whitespace so those options
are available.
Parameters:
line_index int, The index of the line number
line_triples [(int, str, str)] List of lines in the file. First
entry is the line number in the file
(starting at one, not zero).
Second is the valid data on the line.
Third is the entire line (including
comments) also used in error messages.
log handle, The handle of the log file
lstrip bool, If True, remove whitespace at the LHS
rstrip bool, If True, remove whitespace at the RHS
Returns: None
'''
(line_number, line_data, line_text) = line_triples[line_index]
ErrorOnLine(line_number, line_text, log, lstrip, rstrip)
return()
def WriteError(err_num, err_text, log):
'''Print a line of error text to the screen, using the
number err_num. The format of the line is similar to
the first line of each error in SES v4.1. This is so
that people like me (who look for errors in SES output
by searching for '*er' in the PRN file) can use the same
method here.
Write the same message to the logfile.
Parameters:
err_num (int), The number of this error message
err_text (str), The text of the error message
log (handle), The handle of the log file
Returns: None
'''
message = "> *Error* type " + str(err_num)
print(message)
log.write(message + "\n")
WriteMessage(err_text, log)
return()
def WriteMessage(message_text, log):
'''Print a message to the screen and to the logfile. We
sometimes call this directly and sometimes we call it
from writeError. It depends on the circumstances: some
error messages (like name clashes) include an error
number, some don't.
Parameters:
message_text (str), The text of the message
log (handle), The handle of the log file
Returns: None
'''
print(message_text)
log.write(message_text + "\n")
return()
def DudConstant(const_number, const_text, log):
'''An error occurred with a number. The number on the line was
a substitute entry (a constant). This routine tells the user the
line that was in error. It is mostly used in Hobyah.
'''
err = ('> This line of input referenced an entry in one\n'
'> the constants blocks.')
WriteMessage(err, log)
ErrorOnLine(const_number, const_text, log, False)
return()
def WriteOut(line, handle):
'''Write a line to an output file (or to the logfile) and add
a carriage return.
Parameters:
line str, A line of text
handle handle, Handle to the file being written to
Returns: None
'''
handle.write(line + '\n')
return
def OopsIDidItAgain(log, file_name = ""):
'''Write a spiel to the terminal and to the log file telling the
user that their input file has managed to break the program
in an unexpected way and they should raise a bug report.
Parameters:
log (handle), The handle of the log file
file_name (str), An optional file name
Returns: None
'''
import sys
err = ('> Something unexpected occurred during your run\n'
'> and was caught by a sanity check. There is\n'
'> a brief description of what happened above.\n'
'>\n'
'> This is not the usual "here is what happened\n'
'> and this is how you might go about fixing it"\n'
"> error message. Because you can't fix it - this\n"
'> error can only be sorted out by patching the\n'
'> program.\n'
'>\n')
if file_name != "":
err = err + ('> Please raise a bug report and attach the input\n'
'> file "' + file_name + '".')
WriteMessage(err, log)
PauseFail()
def GetFileData(file_string, default_ext, debug1):
'''Take a string (which we expect to be a file name with or without
a file path). Split it up into its component parts. This is best
explained by example. Say we pass the string
"C:/Users/John.D.Batten/Documents/test_001.txt"
to this routine. We get four strings back:
file_name: test_001.txt
dir_name: C:/Users/John.D.Batten/Documents/
file_stem: test_001
file_ext: .txt
In some cases (when we are running from within an interpreter or
an IDE) we will just have the file name and an empty string for
dir_name. In that case we use the current working directory.
We add a trailing "/" to dir_name as we never use it without
something else appended to it.
Parameters:
file_string (str), A file_name or path and file_name
default_ext (str), The extension to return if none was given
debug1 (bool), The debug Boolean set by the user
Returns:
file_name (str), file_name with extension, no path
dir_name (str), Nothing but the path, incl. trailing /
file_stem (str), file_name without extension or path
file_ext (str), File extension only
'''
import os
file_name = os.path.basename(file_string)
dir_name = os.path.dirname(file_string)
if dir_name == '':
# We didn't give a directory name. We are likely
# running in Terminal or an interpreter session, so
# use the current directory.
dir_name = os.getcwd()
file_stem, file_ext = os.path.splitext(file_string)
if file_ext == "":
# We didn't give the file extension (this is common
# when running in Terminal). We make the extension
# the default extension passed to us. If a file with
# that extension doesn't exist, we'll catch it when
# we try to open it.
file_ext = default_ext
file_name = file_name + file_ext
if debug1:
print("file_name:", file_name)
print("dir_name: ", dir_name + "/")
print("file_stem:", file_stem)
print("file_ext: ", file_ext)
return(file_name, dir_name + "/", file_stem, file_ext)
def GetUserQA():
'''Interrogate the OS to get the name of the user running this
process and build a string that can be used for QA - the date,
the time and the user.
Parameters: none
Returns:
user (str), The current user's login name
when)who (str), A QA string giving the date and
time of the run and the name of
the user who ran it
'''
import sys
import os
import datetime
if sys.platform == 'win32':
user = os.getenv('username')
else:
# This works on macOS and Linux
user = os.getenv('USER')
# Build a QA string of the date, the time and the user's
# name, to give some traceability to the output and in the
# log file.
iso_date = datetime.datetime.now().isoformat()
year = iso_date[:4]
month = int(iso_date[5:7])
day = int(iso_date[8:10])
month_dict = {1: "Jan", 2: "Feb", 3: "Mar", 4: "Apr",
5: "May", 6: "Jun", 7: "Jul", 8: "Aug", 9: "Sep",
10: "Oct", 11: "Nov", 12: "Dec"}
month_text = month_dict[month]
when_who = (iso_date[11:16] + ' on '
+ str(day) + " " + month_text + " " + year + ' by '
+ user
)
return(user, when_who)
def PauseIfLast(file_num, file_count):
'''We have printed an error message to the runtime screen. Check
if this is the last file in the list and pause if it is.
Parameters:
file_num (int), The number of the current file
file_count (int), The total count of files run simultaneously
Returns: None
'''
if file_num == file_count:
import sys
# This is the last file in the list. If we are on
# Windows we ought to pause so that the reader can
# read the message.
if sys.platform == 'win32':
import os
os.system("pause")
return()
def SplitTwo(line):
'''Take a string and split it up, return the first two entries
converted to lower case. If there is only one entry in it pad
the pair out, so that we don't have to keep testing the length
of lists.
Parameters:
line (str), A line from the input file
Returns:
two_list (list), The first two words in the line
'''
# Get the first two entries
two_list = line.lower().split(maxsplit = 3)[:2]
# Add a second entry if needed. Note that we don't have
# any entries with zero entries, we stripped those out
# earlier.
if len(two_list) == 1:
two_list.append("")
return(two_list)
def CheckVersion():
'''Check the version of Python we are running. It faults if we
are using a version below 3.6 and returns None if we are using
3.6 or higher. We need the entries in dictionaries to be in
the order they were entered, which is how 3.6 and higher do it.
Anything below 3.6 has random order.
Parameters: none
Returns: None
'''
import sys
version = float(".".join([str(num) for num in sys.version_info[:2]]))
if version < 3.6:
print("> You need to update your version of Python to a newer\n"
"> one. This script will only run with Python version 3.6\n"
"> or higher. You are running it on Python " + str(version)
+ ".\n"
"> Update your version of Python and try again.")
PauseFail()
return()
def Reverse(string):
'''Take a string and return a reversed copy of the string.
Parameters:
string (str)
Returns:
gnirts (str), The string, reversed.
'''
# This is extended slice syntax from Stack Overflow question 931092.
return(string[::-1])
def FloatText(value):
'''Take a floating point number and turn it into a string
suitable for printing. Remove spurious trailing digits.
Some floating point numbers have spurious digits due to
the conversion between base 2 and base 10, such as
0.037755795455000001 and 0.018877897727999998.
This routine seeks numbers with "000" and "999" in the
slice [-4:-1] and rounds the text to be printed to a
suitable extent, e.g. 0.037755795455 and 0.018877897728.
Parameters:
value (float)
Returns:
text_value (str), The string of the number, perhaps
truncated.
'''
text_value = str(value)
size = len(text_value)
if size > 15:
# The string is so long that we may have a value
# that has a floating point mismatch. Check for a
# group of three zeros or nines at the end (three is
# an arbitrary choice).
if text_value[-4:-1] == "000":
# Knock off the final digit and let the float
# function take care of all the trailing zeros.
text_value = str(float(text_value[:-1]))
elif text_value[-4:-1] == "999":
# Figure out where the decimal point is.
dec_pt = text_value.find(".")
if dec_pt >= 0:
# There was a decimal point in the number.
# Round at one of the three trailing nines,
# the round function will consume them all.
round_to = size - dec_pt - 2
text_value = str(round(value,round_to))
return(text_value)
def Interpolate(x1, x2, y1, y2, x_mid, extrapolate = False, log = None):
'''Do linear interpolation with four values. Optionally allow
extrapolation.
Parameters:
x1 float, First value on the X-axis
x2 float, Second value on the X-axis
y1 float, First value on the Y-axis
y2 float, Second value on the Y-axis
x_mid float, X-value we want Y for
extrapolate bool, If False, print an error message and
return None if x_mid < x1 or x_mid > x2
log handle, Handle of a logfile to write to
Returns:
y_mid float, The interpolated (or extrapolated) Y value
Errors:
Aborts with 1024 if we attempt to extrapolate without being
allowed to, printing the message to the screen. If there
is a logfile handle given it writes the message to the logfile
too and returns None to the calling routine. If no log handle
is given it aborts like 1021 to 1023 do.
'''
if (x1 <= x_mid <= x2) or extrapolate:
y_mid = y1 + (y2 - y1) / (x2 - x1) * (x_mid - x1)
else:
err1 = ("Ugh, tried to extrapolate while forbidden to do so. "
"Details are:\n")
num_format = "{:>15.5g}"
if x_mid < x1:
err2 = ("Y-values: "
+ num_format.format(y1)
+ " " + num_format.format(y2) + "\n"
+ "X-values:" + num_format.format(x_mid)
+ " " + num_format.format(x1)
+ " " + num_format.format(x2))
else:
err2 = ("Y-values: "
+ num_format.format(y1)
+ " " + num_format.format(y2) + "\n"
+ "X-values:"
+ " " + num_format.format(x1)
+ " " + num_format.format(x2)
+ " " + num_format.format(x_mid))
if log is not None:
print("writing to log")
WriteError(1024, err1 + err2, log)
log.close()
return(None)
else:
print("> *Error* type 1024")
print(err1 + err2)
PauseFail()
return(y_mid)
def FormatOnLines(my_list):
'''Take a list of keywords and format them into a string of text
with lines no shorter than 45 characters long, with each word
separated by ", " and ending with "and <last entry>.".
Return the string. Used to give lists of valid keywords in
error messages.
Parameters:
my_list [] a list of keywords (we assume they are all strings).
Returns:
line str lines of text with the keywords, properly formatted.
'''
len_list = len(my_list)
if len_list == 1:
line = "> " + my_list[0] + "."
else:
line = "> "
for index, entry in enumerate(my_list):
if index == (len_list - 2):
# This is the 2nd last entry, we don't append
# a comma
line = line + entry + ' '
elif index == (len_list - 1):
# This is the last entry, we include "and".
line = line + "and " + entry + "."
else:
# Just a standard entry.
line = line + entry + ", "
# Check if we have more to add and the line is
# long enough. Start a new line if it is.
if (index != (len_list - 1) and
len(line.split(sep = "\n")[-1]) > 45):
line = line + "\n> "
return(line)
def LogBlock(dictionary, block_name, debug1, log):
'''Take a dictionary made from a block and text describing the
block and write it to the logfile. If the
Parameters:
dictionary dict A dictionary.
block_name str The name of the block
debug1 bool The debug Boolean set by the user.
log handle The handle of the logfile.
Returns:
None
'''
# Write the dictionary to the logfile (and if the debug Boolean
# is set, to the screen too).
header = "Added " + block_name + ":"
WriteOut(header, log)
if debug1:
print(header)
for key in dictionary:
value = dictionary[key]
entry = " " + key + ": " + str(value)
WriteOut(entry, log)
if debug1:
print(entry)
return()