diff --git a/read_in.py b/read_in.py new file mode 100644 index 0000000..309577a --- /dev/null +++ b/read_in.py @@ -0,0 +1,124 @@ +from emis_funky_funktions import * + +from itertools import chain +from typing import List, NamedTuple + +def meters_to_millimeters(meters: float) -> int: + """ + Convert meters (floating point) to millimeters (int) + + >>> meters_to_millimeters(1.9333473e+02) + 193334 + + >>> meters_to_millimeters(1.0) + 1000 + """ + return int(1000 * meters) + +def read_single_elevation(dat: str) -> Result[int, str]: + """ + Read in a single elevation + + Expects a single, stringified floating point value, in meters, without a unit, e.g. + "2.057e+02". If there is a parsing error, returns an error containing the input + string. Otherwise, returns the elevation in millimeters. + + >>> read_single_elevation("1.9333473e+02") + Ok(val=193334) + + >>> read_single_elevation("1.0") + Ok(val=1000) + + >>> read_single_elevation("10.0m") + Err(err='10.0m') + """ + return map_res( + meters_to_millimeters, # fuck floating points + try_(replace(dat), float, dat) + ) + +def read_elevation_line(line: str) -> Result[Iterator[int], Tuple[int, str]]: + """ + Reads a line of elevations + + Each elevation should be in the format specified by `read_single_elevation`. Every + element of the line is parsed and combined into an iterator, which is returned. If + any parsing errors are encountered, the first error is returned. It takes the form of + a tuple containing the column of the error (in terms of whitespace-deliniated words, + not characters) and the string which could not be parsed. + + Recall from the definition of `read_single_elevation()` that the return value is an + integer number of millimeters. + + As per the problem specification, the last five columns of the line are not read or + parsed. Parsing errors in these columns therefor cannot be encountered, and any data + in these columns will not be returned. + + >>> map_res(list, read_elevation_line("1.9e2 18e1 181 179.1 ignored 170.1 1.8e2 18i+2 20e1")) + Ok(val=[190000, 180000, 181000, 179100]) + + >>> map_res(list, read_elevation_line("1.9e2 18e 181 179.1 ignored 170.1 1.8e2 18i+2 20e1")) + Err(err=(2, '18e')) + """ + return sequence([ + map_err( + lambda err_text: (col_no + 1, err_text), + read_single_elevation(elem) + ) + # As per spec, we drop the last five entries of the file. + for (col_no, elem) in enumerate(line.split()[:-5]) + ]) + +class ErrorLocation(NamedTuple): + "The location of an error within file" + + line_no: int + "The line on which the error occurred" + + col_no: int + "The column (1-indexed number of whitespace deliniated words) on which the error occurred" + + invalid_entry: str + "The text which failed to parse as a floating point value" + +def read_elevations(lines: str) -> Result[Iterator[int], ErrorLocation]: + """ + Read an entire elevation file into a list + + This parses each line in of the file using `read_elevation_line()`. Errors + encountered are reported using an `ErrorLocation`. The resulting list is + single-dimensional, but can be indexed arbitrarily, meaning for an input file with a + constant number of columns per line, this data can be accessed as if it were a 2d + square array. + + However, keep in mind that, as with `read_elevation_line()`, the last five columns of + each line are dropped/ignored, and are therefor not present in the returned data. + + >>> map_res(list, read_elevations(''' + ... 1 2 3 4 5 6 7 8 9e 10 + ... 11 2.0 3e1 4 5 6 7 bwa 9 10 + ... 11 2.0 3e0 4 5 ign err 8 9 10 + ... ''')) + Ok(val=[1000, 2000, 3000, 4000, 5000, 11000, 2000, 30000, 4000, 5000, 11000, 2000, 3000, 4000, 5000]) + + >>> map_res(list, read_elevations(''' + ... 1 2 3 4 5 6 7 8 9e 10 + ... 11 ERR 3e1 4 5 6 7 bwa 9 10 + ... 11 2.0 3e0 4 5 ign err 8 9 10 + ... ''')) + Err(err=ErrorLocation(line_no=2, col_no=2, invalid_entry='ERR')) + """ + return map_res( + chain.from_iterable, + sequence([ + map_err( + lambda err: ErrorLocation(line_no + 1, *err), + read_elevation_line(line) + ) + for (line_no, line) in enumerate(lines.strip().split('\n')) + ]) + ) + +if __name__ == '__main__': + import doctest + doctest.testmod() \ No newline at end of file