Skip to content

Instantly share code, notes, and snippets.

@usr-ein
Created April 11, 2021 17:33
Show Gist options
  • Save usr-ein/09d923b9fc2860637a87fa223935231d to your computer and use it in GitHub Desktop.
Save usr-ein/09d923b9fc2860637a87fa223935231d to your computer and use it in GitHub Desktop.
Checks for the validity of numbers following the Luhn algorithm
#!/usr/bin/env python3
"""
Checks for the validity of numbers following the Luhn algorithm
Samuel Prevost 2021-04-01
"""
import os
import sys
import numpy as np
import argparse
def main():
"""Main function"""
main_arg_parser = argparse.ArgumentParser(
description="Checks for the validity of numbers following the Luhn algorithm"
)
main_arg_parser.add_argument(
"-i",
"--input",
required=False,
help="Path to a file containing the numbers, or directly the number itself."
"If not provided, or - provided, reads from stdin.",
)
main_arg_parser.add_argument(
"-q",
"--quick",
action="store_true",
help="Quickly quits as soon as an invalid number is found, if any",
)
main_arg_parser.add_argument(
"--csv",
action="store_true",
help="Prints a CSV file to stdout containing the bad numbers founds",
)
main_arg_parser.add_argument(
"-v",
"--verbose",
action="store_true",
help="Displays helpful information along",
)
args = main_arg_parser.parse_args()
# Flag that will get updated as we go along
all_good = True
if args.csv:
print("Incorrect Numbers")
if args.input is not None and args.input != "-" and not os.path.isfile(args.input):
args.input = "".join([a for a in args.input if a.replace(" ", "").isnumeric()])
valid = check_numbers([args.input], out_csv=args.csv, verbose=args.verbose)
if valid:
if args.verbose:
print("Valid number", file=sys.stderr)
sys.exit(0)
else:
if args.verbose:
print("Invalid number", file=sys.stderr)
sys.exit(1)
else:
iterator = None
if args.input is None or args.input == "-":
iterator = iter(sys.stdin)
elif os.path.isfile(args.input):
f = open(args.input, "r")
iterator = iter(f)
else:
print(f"Invalid argument for --input: {args.input}")
sys.exit(1)
try:
buffer_size = 3000
lines = []
for i, line in enumerate(iterator):
if len(lines) < buffer_size:
line = line.replace("\n", "")
if len(line) == 0:
continue
lines.append(line)
else:
all_good &= check_numbers(
lines, out_csv=args.csv, verbose=args.verbose
)
lines = []
if args.quick and not all_good:
if args.verbose:
print(
f"Some numbers were invalid before line {i+1}",
file=sys.stderr,
)
break
if len(lines) > 0:
all_good &= check_numbers(lines, out_csv=args.csv, verbose=args.verbose)
lines = []
if all_good and args.verbose:
print("All number were valid", file=sys.stderr)
except KeyboardInterrupt:
sys.stdout.flush()
finally:
if hasattr(iterator, "close") and callable(iterator.close):
iterator.close()
if args.verbose:
print("Done.", file=sys.stderr)
# True ==> 1 ==> error exit code
# False ==> 0 ==> all good exit code
# hence the inversion
sys.exit(int(not all_good))
def check_numbers(numbers, out_csv=False, verbose=False):
digits = []
for num in numbers:
digits.append([int(c) for c in num])
arr = np.array(digits, dtype=np.uint64)
# Double every other digit starting from the last, excluded
arr[:, :-1:2] *= 2
# then remove 9 where it's more than one digit
# (equiv to add the two digits if over ten)
arr[:, :-1:2][arr[:, :-1:2] > 9] -= 9
result = np.mod(arr.sum(axis=1), 10) == 0
all_valid = np.all(result)
if not all_valid and (verbose or out_csv):
bad = [num for good, num in zip(result, numbers) if not good]
print("\n".join(bad))
return all_valid
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment