-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathgenerics.py
executable file
·1910 lines (1686 loc) · 78.5 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
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#! python3
#
# Copyright 2020-2024, 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 range 1021-1028, 1041-1044 and
# 1061-1063.
import math
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 The ordinal of the number: '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 ('
+ str(number) + ', a ' + str(type(number)) + ').')
# Abort the run
# raise()
PauseFail()
elif number < 0:
print('> *Error* type 1022 ******************************\n'
'> tried to get the ordinal of a negative number.('
+ str(number) + ').')
# Abort the run
# raise()
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 to convert
Returns:
val 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,
word = "Faulty"):
'''Take the line number of a faulty line of input and the line
itself. Write it to to the screen and to the logfile, if possible.
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_number1 int The line number
line_text1 str The text on the line
log handle The handle of the log file
lstrip bool If True, remove spaces at the
left hand side of the string
rstrip bool If True, remove spaces at the
right hand side of the string
word str A descriptive word to use in
the error message.
Returns: None
'''
if lstrip:
line_text = line_text.lstrip()
if rstrip:
line_text = line_text.rstrip()
message = ('> ' + word + ' line of input (' + Enth(line_number) + ') is\n'
'> ' + line_text)
if log is not str:
# We are calling from a routine that has opened a logfile
log.write(message + "\n")
print(message)
return()
def ErrorOnTwoLines(line_number1, line_text1, line_number2, line_text2,
log, lstrip = True, rstrip = True, word = "Faulty"):
'''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 spaces at the
left hand side of the string
rstrip bool If True, remove spaces at the
right hand side of the string
word str A descriptive word to use in
the error message.
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()
message = ('> ' + word + ' lines of input (' + Enth(line_number1)
+ ' and ' + Enth(line_number2) + ') are\n'
'> ' + line_text1 + '\n'
'> ' + line_text2)
log.write(message + "\n")
print(message)
return()
def ErrorOnThreeLines(line_number1, line_text1, line_number2, line_text2,
line_number3, line_text3, log, lstrip = True,
rstrip = True, word = "Faulty"):
'''Take the line numbers of three 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
line_number3 int The third line number
line_text3 str The text on the third line
log handle The handle of the log file
lstrip bool If True, remove spaces at the
left hand side of the string
rstrip bool If True, remove spaces at the
right hand side of the string
word str A descriptive word to use in
the error message.
Returns: None
'''
if lstrip:
line_text1 = line_text1.lstrip()
line_text2 = line_text2.lstrip()
line_text3 = line_text3.lstrip()
if rstrip:
line_text1 = line_text1.rstrip()
line_text2 = line_text2.rstrip()
line_text3 = line_text3.rstrip()
message = ('> ' + word + ' lines of input (' + Enth(line_number1)
+ ', ' + Enth(line_number2)
+ ' and ' + Enth(line_number3) + ') are\n'
'> ' + line_text1 + '\n'
'> ' + line_text2 + '\n'
'> ' + line_text3)
log.write(message + "\n")
print(message)
return()
def ErrorOnFourLines(line_number1, line_text1, line_number2, line_text2,
line_number3, line_text3, line_number4, line_text4,
log, lstrip = True, rstrip = True, word = "Faulty"):
'''Take the line numbers of four 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
line_number3 int The third line number
line_text3 str The text on the third line
line_number4 int The fourth line number
line_text4 str The text on the fourth line
log handle The handle of the log file
lstrip bool If True, remove spaces at the
left hand side of the string
rstrip bool If True, remove spaces at the
right hand side of the string
word str A descriptive word to use in
the error message.
Returns: None
'''
if lstrip:
line_text1 = line_text1.lstrip()
line_text2 = line_text2.lstrip()
line_text3 = line_text3.lstrip()
line_text4 = line_text4.lstrip()
if rstrip:
line_text1 = line_text1.rstrip()
line_text2 = line_text2.rstrip()
line_text3 = line_text3.rstrip()
line_text4 = line_text4.rstrip()
message = ('> ' + word + ' lines of input (' + Enth(line_number1)
+ ', ' + Enth(line_number2)
+ ', ' + Enth(line_number3)
+ ' and ' + Enth(line_number4) + ') are\n'
'> ' + line_text1 + '\n'
'> ' + line_text2 + '\n'
'> ' + line_text3 + '\n'
'> ' + line_text4)
log.write(message + "\n")
print(message)
return()
def ErrorOnFiveLines(line_number1, line_text1, line_number2, line_text2,
line_number3, line_text3, line_number4, line_text4,
line_number5, line_text5,
log, lstrip = True, rstrip = True, word = "Faulty"):
'''Take the line numbers of five 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
line_number3 int The third line number
line_text3 str The text on the third line
line_number4 int The fourth line number
line_text4 str The text on the fourth line
line_number5 int The fifth line number
line_text5 str The text on the fifth line
log handle The handle of the log file
lstrip bool If True, remove spaces at the
left hand side of the string
rstrip bool If True, remove spaces at the
right hand side of the string
word str A descriptive word to use in
the error message.
Returns: None
'''
if lstrip:
line_text1 = line_text1.lstrip()
line_text2 = line_text2.lstrip()
line_text3 = line_text3.lstrip()
line_text4 = line_text4.lstrip()
line_text5 = line_text5.lstrip()
if rstrip:
line_text1 = line_text1.rstrip()
line_text2 = line_text2.rstrip()
line_text3 = line_text3.rstrip()
line_text4 = line_text4.rstrip()
line_text5 = line_text5.rstrip()
message = ('> ' + word + ' lines of input (' + Enth(line_number1)
+ ', ' + Enth(line_number2)
+ ', ' + Enth(line_number3)
+ ', ' + Enth(line_number4)
+ ' and ' + Enth(line_number5) + ') are\n'
'> ' + line_text1 + '\n'
'> ' + line_text2 + '\n'
'> ' + line_text3 + '\n'
'> ' + line_text4 + '\n'
'> ' + line_text5)
log.write(message + "\n")
print(message)
return()
def ErrorOnManyLines(line_number1, line_text1, line_number2, line_text2,
line_number3, line_text3, line_number4, line_text4,
log, lstrip = True, rstrip = True, word = "Faulty"):
'''Take the line numbers of four faulty lines of input and the lines
themselves. Figure out how many are unique and call a suitable routine
to print one, two, three or four lines.
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
line_number3 int The third line number
line_text3 str The text on the third line
line_number4 int The fourth line number
line_text4 str The text on the fourth line
log handle The handle of the log file
lstrip bool If True, remove spaces at the
left hand side of the string
rstrip bool If True, remove spaces at the
right hand side of the string
word str A descriptive word to use in
the error message.
Returns: None
'''
# Make a list of the unique line numbers.
lines = []
for line in (line_number1, line_number2, line_number3, line_number4):
if line not in lines:
lines.append(line)
# Catch the easy cases first, then do the case where we have three lines.
if len(lines) == 1:
ErrorOnLine(line_number1, line_text1, log, lstrip, rstrip, word)
elif len(lines) == 2:
ErrorOnTwoLines(line_number1, line_text1,
line_number4, line_text4,
log, lstrip, rstrip, word)
elif len(lines) == 4:
ErrorOnFourLines(line_number1, line_text1, line_number2, line_text2,
line_number3, line_text3, line_number4, line_text4,
log, lstrip, rstrip, word)
elif line_number2 == line_number1:
ErrorOnThreeLines(line_number1, line_text1,
line_number3, line_text3, line_number4, line_text4,
log, lstrip, rstrip, word)
else:
ErrorOnThreeLines(line_number1, line_text1, line_number2, line_text2,
line_number4, line_text4,
log, lstrip, rstrip, word)
return()
def ErrorOnLine2(line_index, line_triples, log, lstrip = True, rstrip = True,
word = "Faulty"):
'''Take the line index 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, word)
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) + ' ' + '*'*30
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 WriteMessage2(message_text, log):
'''Print a message to the screen with "> " prepended to it.
Write the same to the logfile without anything prepended.
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 WriteMessage3(message_text, debug1, log):
'''Print a message to the screen with "> " prepended to it if
the debug Boolean is True. Write the same to the logfile without
anything prepended.
Parameters:
message_text str The text of the message
log handle The handle of the log file
Returns: None
'''
if debug1:
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, default_path = ''):
'''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 directory given by "default_path"
unless it is empty ('') in which case we use the current working
directory instead.
If we are on Windows, convert all the backslashes to forward slashes.
We add a trailing "/" to dir_name as we never use it without
something else appended to it.
Some paths can start with a tilde character, '~'. This is
replaced by the name of the user's home folder.
If the last character in the strin is "." we remove it and use the
default filename extension. This is handy when a Terminal session
autocompletes a filename up to the "." in the file name extension.
Parameters:
file_string str A file_name, or the path and
the file_name.
default_ext str The extension to return if
none was given
debug1 bool The debug Boolean set by
the user
default_path str A path that may be prepended.
Typically used when looking
for image files or .csv
files in a folder relative
to the folder where an input
file is rather than the
current working directory.
It must end in '/'.
Returns:
file_name str File_name with extension and
no path.
dir_name str Nothing but the path, incl.
a trailing '/'
file_stem str File_name without extension
or path
file_ext str File extension only
'''
import os
import sys
# Remove the trailing ".", if there is one.
if file_string[-1] == ".":
file_string = file_string[:-1]
file_name = os.path.basename(file_string)
dir_name = os.path.dirname(file_string)
system = sys.platform
# Check the path in the file string (dir_name). If they specified a
# relative path, prepend the path to either the default path specified
# by the calling routine or to the current directory if the default
# path is ''.
# This allows a user to have an input file in a folder structure
# such as "/Users/foo/work/21351-A2W/calcs/TM5/" and specify an
# image file as
#
# filename ../../image.png
#
# and it will turn into a path like
# /Users/foo/work/21351-A2W/calcs/TM5/../../logo.png
#
# which is equivalent to
# /Users/foo/work/21351-A2W/logo.png
#
# This to allow users to keep logo.png in /Users/foo/work/21351-A2W and
# not have to put copies of it into every calculation subfolder.
if len(dir_name) > 0:
# The user did have a path. Check if it was a full path or part
# of a path. We do this in two parts, first for macOS and linux
# then for Windows.
if ( (system != 'win32' and dir_name[0] != '/') or
(system == 'win32' and
len(dir_name) > 1 and dir_name[1] != ":") ):
# The user did not specify a drive letter (on windows)
# or start the path with a '/' (root directory on macOS)
# so they did not give a full path. We need to prepend
# a default path of some kind. Figure out what.
if dir_name[0] == '~':
# The user started the directory string with the character
# that represents their home folder. Replace '~' with it,
# add the rest of the path and finish it with a forward
# slash.
dir_name = GetUserHome() + dir_name[1:] + '/'
elif default_path != '':
# The user had a folder name or a ".." at the start of
# the path and they set a default path. Prepend it.
#
dir_name = default_path + dir_name + '/'
else:
# The user had a folder name or a ".." at the start of
# the path and they did not set a default path. Prepend
# the current working directory's path.
dir_name = os.getcwd() + '/' + dir_name + '/'
else:
# The user set a full path. Add a trailing '/'.
dir_name = dir_name + '/'
elif default_path == '':
# The user did not set any path in the input file and the calling
# routine did not provide a default path. Use the current directory.
dir_name = os.getcwd() + '/'
else:
# The user did not set any path in the input file but the calling
# routine provided a default path. Use it. We add a '/' if one
# was not given.
if default_path[-1] not in ('\\', '/'):
dir_name = default_path + '/'
else:
dir_name = default_path
# If we are on Windows, convert all backslashes to forward slashes.
if system == 'win32':
dir_name = dir_name.replace('\\', '/')
file_stem, file_ext = os.path.splitext(file_name)
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("default_path: ", default_path)
print("file_stem:", file_stem)
print("file_ext: ", file_ext)
return(file_name, dir_name, file_stem, file_ext)
def GetUserHome():
'''Interrogate the OS to get the name of the home directory of
the user running the script. This is used to replace tilde (~)
in filenames that include the tilde in a filepath.
Parameters: none
Returns:
home str The current user's home
folder.
'''
import sys
import os
if sys.platform == 'win32':
# This works on Windows 10 Home 10.0.19044, not sure if it
# breaks on older/newer systems.
home = os.getenv('HOMEDRIVE') + os.getenv('HOMEPATH')
else:
# This works on macOS and Linux.
home = os.getenv('HOME')
return(home)
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.
time_text = TimePlusDate(datetime.datetime.now())
when_who = time_text + ' by ' + user
return(user, when_who)
def TimePlusDate(timestamp):
'''Take a datetime class (year, month, day, hours, minutes, seconds,
microseconds) and turn it into something a human can understand,
such as "16:39 on 1 Sep 2021".
Parameters:
timestamp A datetime instance Date and time from a
system call
Returns:
result str The date and time in human-readable
form
'''
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[timestamp.month]
result = ("{:02d}".format(timestamp.hour) + ":" +
"{:02d}".format(timestamp.minute) + ' on '
+ str(timestamp.day) + " " + str(month_text) + " "
+ str(timestamp.year)
)
return(result)
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.7 and returns None if we are using
3.7 or higher.
Parameters: none
Returns: None
'''
import sys
major = sys.version_info.major
minor = sys.version_info.minor
if major < 3 or minor < 7:
print("> You need to update your version of Python to a newer\n"
"> one. This script will only run with Python version 3.7\n"
"> or higher. You are running it on Python "
+ str(major) + '.' + str(minor) + ".\n"
"> Update your version of Python and try again.")
PauseFail()
return(None)
def Reverse(string):
'''Take a string and return a reversed copy of the string.
Parameters:
string str A string
Returns:
gnirts str The string, reversed.
'''
# This is extended slice syntax from Stack Overflow question 931092.
return(string[::-1])
def FloatText(value, accuracy = 10):
'''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 [-5:-2] and rounds the text to be printed to a
suitable extent, e.g. 0.037755795455 and 0.018877897728.
The optional parameter 'accuracy' defines how many numbers after
the decimal point the rounding starts (default 10). This lets
us avoid rounding (say) 0.1345699921 to 0.13457.
string str A string
Parameters:
value float A number, which may have an
enormous amount of digits.
Returns:
text_value str The string of the number,
possibly truncated.
'''
if type(value) is str:
# Occasionally this routine will be sent a string of numbers and
# a text value. If this is text value, return it unchanged.
text_value = value
else:
text_value = str(value)
# Figure out where the decimal point is. Doing it here guards
# against cases where this routine is sent an integer. If an
# integer is sent (no decimal point), dec_pt is set to -1.
dec_pt = text_value.find(".")
if dec_pt > -1:
# Get the count of digits after the decimal point
size = len(text_value) - dec_pt
if size > accuracy:
# The count of digits to the right of the decimal 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[-5:-2] == "000":
# Knock off the final digit and let the float
# function take care of all the trailing zeros.
text_value = str(float(text_value[:-2]))
elif text_value[-5:-2] == "999":
# 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 = min(accuracy, size - 3)
text_value = str(round(value, round_to))
return(text_value)
def RoundText(value, roundto):
'''Take a floating point number, round it to a given count of
decimal places and turn it into a string suitable for printing.
If the number ends with ".0", take off the ".0". This is mostly
used for vehicle flowrates in printed messages.
string str A string
Parameters:
value float A number, which may have an
enormous amount of digits.
Returns:
text_value str The string of the number,
truncated.
'''
if type(value) is str:
# Occasionally this routine will be sent a string of numbers and
# a text value. If this is text value, return it unchanged.
text_value = value
else:
text_value = str(round(value, roundto))
if text_value[-2:] == ".0" and len(text_value) > 2:
# Crop off the ".0". We include the length check in
# case the string ".0" comes back (can't see how it would
# but it doesn't hurt to check).
text_value = text_value[:-2]
return(text_value)
def AlignListPrint(arrays):
'''Take a list of lists of numbers. Each sub-list must be the same
length (we're using zip functions here). For each index in the
sub-lists, figure out which takes up the most space when converted
to text. Create a new list of lists. Each sub-list contains the
original number as a string, formatted to be centred on the widest
width for that index.
'''
# Get the widths of the first list in the arrays.
widths = [len(str(entry)) for entry in arrays[0]]
# Loop over all the other lists in the arrays.
for my_list in arrays[1:]:
# Check if any of the values in this list are wider than the
# current value.
for index, entry in enumerate(my_list):
candidate = len(str(entry))
if candidate > widths[index]:
widths[index] = candidate
# Now get a list of strings in which all the numbers are centred
# on the maximum width for their index. When these lists are
# printed by the routine that called here, the numbers will line up.
listoftexts = []
for my_list in arrays:
texts = [str(entry).center(width) for entry, width in zip(my_list, widths)]
listoftexts.append('[ ' + ', '.join(texts) + ']')
return(listoftexts)
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