Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Alan Grissett - Finished assignment, with complex currency conversions. #12

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
f8a83fa
First test passed. If from == to then value is returned.
grissett83 Jan 20, 2015
318dc57
Passed second test. Correct rate extracted from rate list.
grissett83 Jan 20, 2015
2dc1d35
Passed second test. Correct rate extracted from rate list.
grissett83 Jan 20, 2015
d41653d
Passed third test. get_rate correctly returns rates to the convert f…
grissett83 Jan 20, 2015
b46da3f
Added test for values other than one. Added support for reverse exch…
grissett83 Jan 20, 2015
d1474c6
Added ValueError for values which do not have a conversion rate in th…
grissett83 Jan 20, 2015
cd04cad
Added if __name__ = __main__ to test manually which entailed making u…
grissett83 Jan 20, 2015
b202f4a
Added build graph function and test to verify the graphs contents.
grissett83 Jan 21, 2015
931828e
Added function and test to get start locations for all possible graph…
grissett83 Jan 21, 2015
7716f37
Added shortest path search for graph. Added test to ensure the path …
grissett83 Jan 21, 2015
57d7461
Updated search function to ignore paths which have from in the second…
grissett83 Jan 21, 2015
0e76df5
FINISHED complex conversions. Checked data with online converters to…
grissett83 Jan 21, 2015
840f12e
Fixed a logic error which caused paths of length 2 to be thrown out. …
grissett83 Jan 21, 2015
c2faaa9
Reorganized functions in file. Reverted my last 'fix' because my ori…
grissett83 Jan 21, 2015
9362df1
Fixed pep8 errors.
grissett83 Jan 21, 2015
4208222
Fixed shortest path algorithm. Made it ignore cases where the length…
grissett83 Jan 26, 2015
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
194 changes: 194 additions & 0 deletions currency_converter.py
Original file line number Diff line number Diff line change
@@ -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)
120 changes: 120 additions & 0 deletions test_currency_converter.py
Original file line number Diff line number Diff line change
@@ -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