diff --git a/currency_converter.py b/currency_converter.py new file mode 100644 index 0000000..05ae880 --- /dev/null +++ b/currency_converter.py @@ -0,0 +1,194 @@ +# currency_converter.py +# Alan Grissett +# Set of funcitons to convert a value from one currency to another. +# + + +def build_graph(rates): + """Builds a graph for use in converting between two currencies with no + explicit link""" + graph = {} + for vertex in rates: + graph[vertex] = [] + for edge in rates: + if vertex[0] in edge or vertex[1] in edge: + graph[vertex].append(edge) + return graph + + +def convert(rates, value, from_x, to_y): + """Takes a list of rates, a currency value, a string to specify which + currency to convert from and a string to specify which currency to + convert to.""" + try: + if from_x == to_y: + return value + else: + return value * get_rate(rates, from_x, to_y) + except ValueError as e: + print(str(e)) + + +def compare_paths(current_path, best_path): + """Compares the lengths of the current path with the best found path. + If the current path is shorter returns that, else returns the best path""" + if best_path: + if len(current_path) < len(best_path): + return current_path + else: + return best_path + return current_path + + +def complex_rate_find(rates, from_x, to_y): + """Launching function to determine path for currency conversion when + no explicit link is found.""" + graph = build_graph(rates) + start_locations = get_locations(graph, from_x) + end_locations = get_locations(graph, to_y) + best_path = find_best_path(graph, start_locations, end_locations, + from_x, to_y) + rate = determine_rate_from_path(best_path, from_x, to_y) + return rate + + +def determine_rate_from_path(best_path, from_x, to_y): + """Calculates the conversion rate based on the path provided by the graph + traversal""" + rate = 1 + for node in best_path: + rate = extract_rate(best_path, node, from_x, to_y, rate) + return rate + + +def display_rates(rates): + """Displays a list of current conversion rates in the console""" + print("Current conversion rates:\n") + for rate in rates: + print("{} to {} - Current Rate : {}".format(rate[0], + rate[1], + rate[2])) + + +def extract_first_rate(node, from_x, rate): + """Extracts the rate for the first node of the transversal where from_x + is included in the node""" + if node[0] == from_x: + rate *= node[2] + else: + rate *= 1 / node[2] + return rate + + +def extract_last_rate(node, to_y, rate): + """ Extracts the rate for the last node of the transversal where to_y is + included in the node""" + if node[0] == to_y: + rate *= 1 / node[2] + else: + rate *= node[2] + return rate + + +def extract_middle_rate(best_path, node, rate): + """Ensures that the rates are calculated correctly by looking ahead one + node to determine which currency should be converted to/from""" + if node[0] in best_path[best_path.index(node) + 1]: + rate *= 1 / node[2] + else: + rate *= node[2] + return rate + +def extract_rate(best_path, node, from_x, to_y, rate): + """Extracts the correct rate based on the position of the current value + and the destination value""" + if from_x in node: + rate = extract_first_rate(node, from_x, rate) + elif to_y in node: + rate = extract_last_rate(node, to_y, rate) + else: + rate = extract_middle_rate(best_path, node, rate) + return rate + + +def find_best_path(graph, start_locations, end_locations, from_x, to_y): + """Takes start locations and end locations and iterates through all + combinations to determine the best start/end combination and returns + the best path""" + best_path = [] + for start in start_locations: + for end in end_locations: + path = find_path(graph, start, end, from_x, to_y) + if path: + best_path = compare_paths(path, best_path) + return best_path + + +def find_path(graph, start, end, from_x, to_y, path=[]): + """Finds the shortest path between two currencies with no explicit link""" + path = path + [start] + if start == end or to_y in path: + return path + shortest = [] + shortest = traverse_nodes(graph, start, end, path, shortest, from_x, to_y) + return shortest + + +def get_input(): + """Prompts the user to provide a starting currency, ending currency and + amount to be converted""" + start = input("Starting currency: ").upper() + end = input("Ending currency: ").upper() + try: + value = int(input("Amount to convert: ")) + return start, end, value + except ValueError: + print("Amount must be a number!") + + +def get_locations(graph, target): + """Determines all possible starting locations for the graph traversal + used to determine non-explicit conversion rates""" + start_locations = [] + for key, item in graph.items(): + if target in key: + start_locations.append(key) + return start_locations + + +def get_rate(rates, from_x, to_y): + """Extracts the required rate from the list of supplied rates""" + for rate in rates: + if rate[0] == from_x and rate[1] == to_y: + return rate[2] + elif rate[1] == from_x and rate[0] == to_y: + return 1 / rate[2] + + return complex_rate_find(rates, from_x, to_y) + + +def traverse_nodes(graph, start, end, path, shortest, from_x, to_y): + """Traverses through the nodes of the graph to determine the shortest + path""" + for node in graph[start]: + if node not in path: + new_path = find_path(graph, node, end, from_x, to_y, path) + if new_path: + if (not shortest or len(new_path) < len(shortest)) and ( + from_x not in new_path[1]): + shortest = new_path + return shortest + +if __name__ == '__main__': + rates = [("EUR", "USD", 1.15504), + ("USD", "JPY", 118.685), + ("GBP", "USD", 1.51590), + ("USD", "CHF", 0.87558), + ("USD", "CAD", 1.20851), + ("EUR", "JPY", 137.087), + ("AUD", "USD", 0.81749), + ("EUR", "INR", 71.40929)] + display_rates(rates) + start, end, value = get_input() + result = convert(rates, value, start, end) + print(result) diff --git a/test_currency_converter.py b/test_currency_converter.py new file mode 100644 index 0000000..eb31f12 --- /dev/null +++ b/test_currency_converter.py @@ -0,0 +1,120 @@ +import currency_converter as converter + + +rates = [("EUR", "USD", 1.15504), + ("USD", "JPY", 118.685), + ("GBP", "USD", 1.51590), + ("USD", "CHF", 0.87558), + ("USD", "CAD", 1.20851), + ("EUR", "JPY", 137.087), + ("AUD", "USD", 0.81749), + ("EUR", "INR", 71.40929)] + + +def test_to_from_equivalence(): + """Test to ensure that if the from and to currencies are the same, then + the value provided is returned.""" + assert converter.convert(rates, 1, "USD", "USD") == 1 + + +def test_rate_extraction(): + """Tests to ensure that the correct rate is being retrieved from the list + of rates.""" + assert converter.get_rate(rates, "EUR", "USD") == 1.15504 + assert converter.get_rate(rates, "USD", "JPY") == 118.685 + assert converter.get_rate(rates, "GBP", "USD") == 1.51590 + assert converter.get_rate(rates, "USD", "CHF") == 0.87558 + + +def test_convert_first_value(): + """Tests to ensure that the get_rate function is correctly passing values + for calculation to the convert function.""" + assert converter.convert(rates, 1, "EUR", "USD") == 1.15504 + + +def test_other_values(): + """Tests values other than 1 to ensure the program is functioning + correctly""" + assert converter.convert(rates, 2, "EUR", "USD") == 2 * 1.15504 + assert converter.convert(rates, 125, "USD", "JPY") == 125 * 118.685 + assert converter.convert(rates, 250, "EUR", "JPY") == 250 * 137.087 + assert converter.convert(rates, 500000, "USD", "CHF") == 500000 * 0.87558 + + +def test_reverse_conversion(): + """Tests to ensure if to_x and from_y are not in the list but to_y from_x + is in the list, the correct conversion rate is returned.""" + assert converter.get_rate(rates, "USD", "EUR") == 1 / 1.15504 + assert converter.get_rate(rates, "JPY", "USD") == 1 / 118.685 + + +def test_value_error(): + """Tests to ensure that a Value Error is raised when there is no exchange + rate for the provided strings in the rate table.""" + try: + converter.get_rate(rates, "EUR", "AUD") + except ValueError: + assert True + + +def test_create_graph(): + """Tests to make sure that graph function correctly creates an exhaustive + graph""" + test_range = [(1, 1), (1, 2), (2, 1), (2, 2)] + + graph = converter.build_graph(test_range) + assert (1, 1) in graph[(1, 1)] + assert (1, 2) in graph[(1, 1)] + assert (2, 1) in graph[(1, 1)] + assert (2, 2) not in graph[(1, 1)] + assert (1, 1) in graph[(1, 2)] + assert (1, 2) in graph[(1, 2)] + assert (2, 1) in graph[(1, 2)] + assert (2, 2) in graph[(1, 2)] + assert (1, 1) in graph[(2, 1)] + assert (1, 2) in graph[(2, 1)] + assert (2, 1) in graph[(2, 1)] + assert (2, 2) in graph[(2, 1)] + assert (1, 1) not in graph[(2, 2)] + assert (1, 2) in graph[(2, 2)] + assert (2, 1) in graph[(2, 2)] + assert (2, 2) in graph[(2, 2)] + + +def test_get_start_locations(): + """Tests to ensure that the function correctly returns all possible start + locations for the graph traversal.""" + test_range = [(1, 1), (1, 2), (2, 1), (2, 2)] + graph = converter.build_graph(test_range) + start_locations = converter.get_locations(graph, 1) + + assert (1, 1) in start_locations + assert (1, 2) in start_locations + assert (2, 1) in start_locations + assert (2, 2) not in start_locations + + +def test_find_shortest_path(): + """Tests to see if the shortest traversal path is found.""" + test_range = [("AUD", "USD"), ("CAD", "AUD"), ("CAD", "USD"), + ("USD", "JPY"), ("CAD", "JPY"), ("AUD", "EUR")] + graph = converter.build_graph(test_range) + shortest_path = converter.find_path(graph, ("AUD", "USD"), ("CAD", "AUD"), + "AUD", "CAD") + assert len(shortest_path) == 3 + + shortest_path = converter.find_path(graph, ("AUD", "USD"), ("CAD", "JPY"), + "AUD", "JPY") + assert len(shortest_path) == 3 + + shortest_path = converter.find_path(graph, ("CAD", "JPY"), ("AUD", "EUR"), + "CAD", "EUR") + assert len(shortest_path) == 4 + + +def test_complex_currency_conversion(): + """Tests the entire complex currency conversion system. These values + were alos verified by looking up current exchange rates online.""" + assert converter.convert(rates, 1, "GBP", "JPY") == 179.9145915 + assert converter.convert(rates, 1, "JPY", "AUD") == 0.010306749408914235 + assert converter.convert(rates, 1, "EUR", "CAD") == 1.3958773904000001