diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..5275cec
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,9 @@
+# Created by .ignore support plugin (hsz.mobi)
+.gradle/
+/.idea/*
+!/.idea/codeStyles
+!/.idea/watcherTasks.xml
+!/.idea/runConfigurations/
+!/.idea/scopes/Code.xml
+build/
+out/
\ No newline at end of file
diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
new file mode 100644
index 0000000..cba1d10
--- /dev/null
+++ b/.idea/codeStyles/Project.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml
new file mode 100644
index 0000000..79ee123
--- /dev/null
+++ b/.idea/codeStyles/codeStyleConfig.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..ad1ab4e
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.java-version b/.java-version
new file mode 100644
index 0000000..3b5b5d8
--- /dev/null
+++ b/.java-version
@@ -0,0 +1 @@
+11.0
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..aabdc03
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,47 @@
+plugins {
+ id 'org.jetbrains.kotlin.jvm' version "$KOTLIN_VERSION"
+}
+
+java.sourceCompatibility = JavaVersion.VERSION_11
+java.targetCompatibility = JavaVersion.VERSION_11
+
+group "to.refactoring.javatokotlin.travelator"
+version "1.0-SNAPSHOT"
+
+repositories {
+ mavenCentral()
+ jcenter()
+ mavenLocal()
+}
+
+dependencies {
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
+
+ implementation "com.fasterxml.jackson.core:jackson-databind:2.10.0"
+ implementation "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.10.0"
+ implementation "com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.10.0"
+
+ testImplementation "org.junit.jupiter:junit-jupiter-api:5.4.2"
+ testImplementation "org.junit.jupiter:junit-jupiter-params:5.4.2"
+ testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:5.5.2"
+ testRuntimeOnly "org.junit.platform:junit-platform-launcher:1.4.2"
+}
+
+tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile) {
+ kotlinOptions {
+ jvmTarget = "11"
+ javaParameters = true
+ freeCompilerArgs = ["-Xjvm-default=all"]
+ }
+}
+
+println("building with Kotlin $KOTLIN_VERSION")
+
+test {
+ useJUnitPlatform {
+ includeEngines "junit-jupiter"
+ }
+ testLogging {
+ events "skipped", "failed"
+ }
+}
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..2802402
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,5 @@
+org.gradle.parallel=true
+org.gradle.caching=false
+
+KOTLIN_VERSION=1.5.0
+kotlin.code.style=official
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..5c2d1cf
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..9d6d2f8
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Sun Mar 15 11:22:46 GMT 2020
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.2.2-all.zip
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStorePath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
\ No newline at end of file
diff --git a/gradlew b/gradlew
new file mode 100755
index 0000000..77564dd
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,188 @@
+#!/usr/bin/env sh
+
+#
+# Copyright 2015 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+ cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"
\ No newline at end of file
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..15e3d84
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,100 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
\ No newline at end of file
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..70144f0
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1 @@
+rootProject.name = 'refactoring-to-kotlin-code'
\ No newline at end of file
diff --git a/src/main/java/travelator/Id.java b/src/main/java/travelator/Id.java
new file mode 100644
index 0000000..b7ee536
--- /dev/null
+++ b/src/main/java/travelator/Id.java
@@ -0,0 +1,42 @@
+package travelator;
+
+import java.util.Objects;
+import java.util.UUID;
+
+public class Id {
+ private final String raw;
+
+ private Id(String raw) {
+ this.raw = raw;
+ }
+
+ public static Id of(String raw) {
+ return new Id(raw);
+ }
+
+ public static String raw(Id id) {
+ return id.raw;
+ }
+
+ public static Id mint() {
+ return Id.of(UUID.randomUUID().toString());
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Id> id = (Id>) o;
+ return raw.equals(id.raw);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(raw);
+ }
+
+ @Override
+ public String toString() {
+ return raw;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/travelator/Location.java b/src/main/java/travelator/Location.java
new file mode 100644
index 0000000..88b08a8
--- /dev/null
+++ b/src/main/java/travelator/Location.java
@@ -0,0 +1,47 @@
+package travelator;
+
+import java.util.Objects;
+
+public class Location {
+ private final Id id;
+ private final String localName;
+ private final String userReadableName;
+
+ public Location(Id id, String localName, String userReadableName) {
+ this.id = id;
+ this.localName = localName;
+ this.userReadableName = userReadableName;
+ }
+
+ public Id getId() {
+ return id;
+ }
+
+ public String getLocalName() {
+ return localName;
+ }
+
+ public String getUserReadableName() {
+ return userReadableName;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Location location = (Location) o;
+ return id.equals(location.id) &&
+ localName.equals(location.localName) &&
+ userReadableName.equals(location.userReadableName);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id, localName, userReadableName);
+ }
+
+ @Override
+ public String toString() {
+ return "Location(" + id + ": " + userReadableName + ")";
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/travelator/itinerary/CostSummary.java b/src/main/java/travelator/itinerary/CostSummary.java
new file mode 100644
index 0000000..30625c3
--- /dev/null
+++ b/src/main/java/travelator/itinerary/CostSummary.java
@@ -0,0 +1,30 @@
+package travelator.itinerary;
+
+import travelator.money.CurrencyConversion;
+import travelator.money.Money;
+
+import java.util.ArrayList;
+import java.util.Currency;
+import java.util.List;
+
+public class CostSummary {
+ private final List lines = new ArrayList<>();
+ private Money total;
+
+ public CostSummary(Currency userCurrency) {
+ this.total = Money.of(0, userCurrency);
+ }
+
+ public void addLine(CurrencyConversion line) {
+ lines.add(line);
+ total = total.add(line.getToMoney());
+ }
+
+ public List getLines() {
+ return List.copyOf(lines);
+ }
+
+ public Money getTotal() {
+ return total;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/travelator/itinerary/CostSummaryCalculator.java b/src/main/java/travelator/itinerary/CostSummaryCalculator.java
new file mode 100644
index 0000000..e63e97c
--- /dev/null
+++ b/src/main/java/travelator/itinerary/CostSummaryCalculator.java
@@ -0,0 +1,45 @@
+package travelator.itinerary;
+
+import travelator.money.ExchangeRates;
+import travelator.money.Money;
+
+import java.util.ArrayList;
+import java.util.Currency;
+import java.util.HashMap;
+import java.util.Map;
+
+import static java.util.Comparator.comparing;
+
+public class CostSummaryCalculator {
+ private final Currency userCurrency;
+ private final ExchangeRates exchangeRates;
+ private final Map currencyTotals = new HashMap<>();
+
+ public CostSummaryCalculator(
+ Currency userCurrency,
+ ExchangeRates exchangeRates
+ ) {
+ this.userCurrency = userCurrency;
+ this.exchangeRates = exchangeRates;
+ }
+
+ public void addCost(Money cost) {
+ currencyTotals.merge(cost.getCurrency(), cost, Money::add);
+ }
+
+ public CostSummary summarise() {
+ var totals = new ArrayList<>(currencyTotals.values());
+ totals.sort(comparing(m -> m.getCurrency().getCurrencyCode()));
+
+ CostSummary summary = new CostSummary(userCurrency);
+ for (var total : totals) {
+ summary.addLine(exchangeRates.convert(total, userCurrency));
+ }
+
+ return summary;
+ }
+
+ public void reset() {
+ currencyTotals.clear();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/travelator/itinerary/Interchange.java b/src/main/java/travelator/itinerary/Interchange.java
new file mode 100644
index 0000000..3017fa7
--- /dev/null
+++ b/src/main/java/travelator/itinerary/Interchange.java
@@ -0,0 +1,78 @@
+package travelator.itinerary;
+
+import travelator.Location;
+
+import java.time.Duration;
+import java.time.ZonedDateTime;
+import java.util.Objects;
+
+public class Interchange {
+ private final Location arrivalLocation;
+ private final ZonedDateTime arrivalTime;
+ private final Location departureLocation;
+ private final ZonedDateTime departureTime;
+
+ public Interchange(Location arrivalLocation, ZonedDateTime arrivalTime, Location departureLocation, ZonedDateTime departureTime) {
+ this.arrivalLocation = arrivalLocation;
+ this.arrivalTime = arrivalTime;
+ this.departureLocation = departureLocation;
+ this.departureTime = departureTime;
+ }
+
+ public static Interchange between(Journey incoming, Journey outgoing) {
+ return new Interchange(
+ incoming.getArrivesAt(), incoming.getArrivalTime(),
+ outgoing.getDepartsFrom(), outgoing.getDepartureTime());
+ }
+
+ public Location getArrivalLocation() {
+ return arrivalLocation;
+ }
+
+ public ZonedDateTime getArrivalTime() {
+ return arrivalTime;
+ }
+
+ public Location getDepartureLocation() {
+ return departureLocation;
+ }
+
+ public ZonedDateTime getDepartureTime() {
+ return departureTime;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Interchange that = (Interchange) o;
+ return arrivalLocation.equals(that.arrivalLocation) &&
+ arrivalTime.equals(that.arrivalTime) &&
+ departureLocation.equals(that.departureLocation) &&
+ departureTime.equals(that.departureTime);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(arrivalLocation, arrivalTime, departureLocation, departureTime);
+ }
+
+ @Override
+ public String toString() {
+ return "RequiredStay{" +
+ "arrivalLocation=" + arrivalLocation +
+ ", arrivalTime=" + arrivalTime +
+ ", departureLocation=" + departureLocation +
+ ", departureTime=" + departureTime +
+ '}';
+ }
+
+ public boolean isAccommodationRequired() {
+ boolean waitIsOvernight =
+ arrivalTime.toLocalDate().isBefore(departureTime.toLocalDate());
+ Duration waitDuration =
+ Duration.between(arrivalTime, departureTime);
+
+ return waitIsOvernight && waitDuration.toHours() >= 6;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/travelator/itinerary/Journey.java b/src/main/java/travelator/itinerary/Journey.java
new file mode 100644
index 0000000..ab80950
--- /dev/null
+++ b/src/main/java/travelator/itinerary/Journey.java
@@ -0,0 +1,117 @@
+package travelator.itinerary;
+
+import travelator.Id;
+import travelator.Location;
+import travelator.money.Money;
+
+import java.time.ZonedDateTime;
+
+/**
+ * A single journey
+ */
+public class Journey {
+ private Id id;
+ private Location departsFrom, arrivesAt;
+ private Id operatorId;
+ private TravelMethod method;
+ private ZonedDateTime departureTime; // in the timezone of from Location
+ private ZonedDateTime arrivalTime; // in the timezone of destination
+ private String operatorClass;
+ private Money price;
+
+ public Journey() {
+ }
+
+ public Journey(
+ Location departsFrom,
+ Location arrivesAt,
+ Id operatorId,
+ TravelMethod method,
+ ZonedDateTime departureTime,
+ ZonedDateTime arrivalTime,
+ String operatorClass,
+ Money price
+ ) {
+ this.id = Id.mint();
+ this.departsFrom = departsFrom;
+ this.arrivesAt = arrivesAt;
+ this.operatorId = operatorId;
+ this.method = method;
+ this.departureTime = departureTime;
+ this.arrivalTime = arrivalTime;
+ this.operatorClass = operatorClass;
+ this.price = price;
+ }
+
+ public Location getDepartsFrom() {
+ return departsFrom;
+ }
+
+ public void setDepartsFrom(Location departsFrom) {
+ this.departsFrom = departsFrom;
+ }
+
+ public Location getArrivesAt() {
+ return arrivesAt;
+ }
+
+ public void setArrivesAt(Location arrivesAt) {
+ this.arrivesAt = arrivesAt;
+ }
+
+ public ZonedDateTime getDepartureTime() {
+ return departureTime;
+ }
+
+ public void setDepartureTime(ZonedDateTime departureTime) {
+ this.departureTime = departureTime;
+ }
+
+ public ZonedDateTime getArrivalTime() {
+ return arrivalTime;
+ }
+
+ public void setArrivalTime(ZonedDateTime arrivalTime) {
+ this.arrivalTime = arrivalTime;
+ }
+
+ public TravelMethod getMethod() {
+ return method;
+ }
+
+ public void setMethod(TravelMethod method) {
+ this.method = method;
+ }
+
+ public Id getOperatorId() {
+ return operatorId;
+ }
+
+ public void setOperatorId(Id operatorId) {
+ this.operatorId = operatorId;
+ }
+
+ public Money getPrice() {
+ return price;
+ }
+
+ public void setPrice(Money price) {
+ this.price = price;
+ }
+
+ public String getOperatorClass() {
+ return operatorClass;
+ }
+
+ public void setOperatorClass(String operatorClass) {
+ this.operatorClass = operatorClass;
+ }
+
+ public Id getId() {
+ return id;
+ }
+
+ public void setId(Id id) {
+ this.id = id;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/travelator/itinerary/Operator.java b/src/main/java/travelator/itinerary/Operator.java
new file mode 100644
index 0000000..906dc16
--- /dev/null
+++ b/src/main/java/travelator/itinerary/Operator.java
@@ -0,0 +1,5 @@
+package travelator.itinerary;
+
+public interface Operator {
+ String getName();
+}
\ No newline at end of file
diff --git a/src/main/java/travelator/itinerary/Route.java b/src/main/java/travelator/itinerary/Route.java
new file mode 100644
index 0000000..2f424a2
--- /dev/null
+++ b/src/main/java/travelator/itinerary/Route.java
@@ -0,0 +1,60 @@
+package travelator.itinerary;
+
+import travelator.Location;
+
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+
+public class Route {
+ private final List journeys;
+
+ public Route(List journeys) {
+ this.journeys = journeys;
+ }
+
+ public int size() {
+ return journeys.size();
+ }
+
+ public Journey get(int index) {
+ return journeys.get(index);
+ }
+
+ public Location getDepartsFrom() {
+ return get(0).getDepartsFrom();
+ }
+
+ public Location getArrivesAt() {
+ return get(size() - 1).getArrivesAt();
+ }
+
+ public Duration getDuration() {
+ return Duration.between(
+ get(0).getDepartureTime(),
+ get(size() - 1).getArrivalTime());
+ }
+
+
+ public void addCostsTo(CostSummaryCalculator calculator) {
+ for (var j : journeys) {
+ calculator.addCost(j.getPrice());
+ }
+ }
+
+
+ public List requiredAccommodation() {
+ var results = new ArrayList();
+
+ for (int i = 1; i < journeys.size(); i++) {
+ var interchange =
+ Interchange.between(journeys.get(i - 1), journeys.get(i));
+
+ if (interchange.isAccommodationRequired()) {
+ results.add(interchange);
+ }
+ }
+
+ return results;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/travelator/itinerary/TravelMethod.java b/src/main/java/travelator/itinerary/TravelMethod.java
new file mode 100644
index 0000000..fef6e87
--- /dev/null
+++ b/src/main/java/travelator/itinerary/TravelMethod.java
@@ -0,0 +1,11 @@
+package travelator.itinerary;
+
+public enum TravelMethod {
+ AIR,
+ SEA,
+ RAIL,
+ BUS,
+ CAR,
+ CARRIAGE,
+ CAMEL;
+}
\ No newline at end of file
diff --git a/src/main/java/travelator/money/CurrencyConversion.kt b/src/main/java/travelator/money/CurrencyConversion.kt
new file mode 100644
index 0000000..782626f
--- /dev/null
+++ b/src/main/java/travelator/money/CurrencyConversion.kt
@@ -0,0 +1,5 @@
+package travelator.money
+
+data class CurrencyConversion(
+ val fromMoney: Money,
+ val toMoney: Money)
\ No newline at end of file
diff --git a/src/main/java/travelator/money/ExchangeRates.kt b/src/main/java/travelator/money/ExchangeRates.kt
new file mode 100644
index 0000000..5a997dd
--- /dev/null
+++ b/src/main/java/travelator/money/ExchangeRates.kt
@@ -0,0 +1,16 @@
+package travelator.money
+
+import java.math.BigDecimal
+import java.util.*
+
+interface ExchangeRates {
+ fun rate(fromCurrency: Currency, toCurrency: Currency): BigDecimal
+
+ @JvmDefault
+ fun convert(fromMoney: Money, toCurrency: Currency): CurrencyConversion {
+ val rate = rate(fromMoney.currency, toCurrency)
+ val toAmount = fromMoney.amount * rate
+ val toMoney = Money(toAmount, toCurrency)
+ return CurrencyConversion(fromMoney, toMoney)
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/travelator/money/ExchangeRatesViaBaseCurrency.kt b/src/main/java/travelator/money/ExchangeRatesViaBaseCurrency.kt
new file mode 100644
index 0000000..9096564
--- /dev/null
+++ b/src/main/java/travelator/money/ExchangeRatesViaBaseCurrency.kt
@@ -0,0 +1,27 @@
+package travelator.money
+
+import java.math.BigDecimal
+import java.math.RoundingMode
+import java.util.*
+
+class ExchangeRatesViaBaseCurrency(
+ private val baseCurrency: Currency,
+ vararg rates: Map.Entry
+) : ExchangeRates {
+
+ private val rates = rates.map { (k,v) -> k to asRate(v) }.toMap()
+
+ override fun rate(fromCurrency: Currency, toCurrency: Currency): BigDecimal {
+ val fromRate = baseRate(fromCurrency)
+ val toRate = baseRate(toCurrency)
+ return toRate.divide(fromRate, RoundingMode.HALF_EVEN)
+ }
+
+ private fun baseRate(c: Currency): BigDecimal {
+ return if (c == baseCurrency) asRate(BigDecimal.ONE) else rates[c]!!
+ }
+
+ private fun asRate(value: BigDecimal): BigDecimal {
+ return value.setScale(6, RoundingMode.UNNECESSARY)
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/travelator/money/Money.kt b/src/main/java/travelator/money/Money.kt
new file mode 100644
index 0000000..dd1c189
--- /dev/null
+++ b/src/main/java/travelator/money/Money.kt
@@ -0,0 +1,60 @@
+package travelator.money
+
+import java.math.BigDecimal
+import java.util.*
+
+class Money private constructor(
+ val amount: BigDecimal,
+ val currency: Currency
+) {
+ override fun equals(other: Any?) =
+ this === other ||
+ other is Money &&
+ amount == other.amount &&
+ currency == other.currency
+
+ override fun hashCode() =
+ Objects.hash(amount, currency)
+
+ override fun toString() =
+ amount.toString() + " " + currency.currencyCode
+
+ fun add(that: Money) = this + that
+
+ operator fun plus(that: Money): Money {
+ require(currency == that.currency) {
+ "cannot add Money values of different currencies"
+ }
+ return Money(this.amount + that.amount, currency)
+ }
+
+ companion object : (BigDecimal,Currency) -> Money {
+ @JvmStatic
+ fun of(amount: BigDecimal, currency: Currency) =
+ this(amount, currency)
+
+ override operator fun invoke(amount: BigDecimal, currency: Currency) =
+ Money(
+ amount.setScale(currency.defaultFractionDigits),
+ currency
+ )
+
+ @JvmStatic
+ fun of(amountStr: String, currency: Currency) =
+ this(amountStr, currency)
+
+ operator fun invoke(amountStr: String, currency: Currency) =
+ invoke(BigDecimal(amountStr), currency)
+
+ @JvmStatic
+ fun of(amount: Int, currency: Currency) =
+ this(amount, currency)
+
+ operator fun invoke(amount: Int, currency: Currency) =
+ invoke(BigDecimal(amount), currency)
+
+ @JvmStatic
+ fun zero(userCurrency: Currency) =
+ invoke(BigDecimal.ZERO, userCurrency)
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/travelator/itinerary/RouteCostTest.java b/src/test/java/travelator/itinerary/RouteCostTest.java
new file mode 100644
index 0000000..189ecba
--- /dev/null
+++ b/src/test/java/travelator/itinerary/RouteCostTest.java
@@ -0,0 +1,97 @@
+package travelator.itinerary;
+
+import org.junit.jupiter.api.Test;
+import travelator.money.ExchangeRates;
+import travelator.money.ExchangeRatesViaBaseCurrency;
+import travelator.money.Money;
+
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.util.Currency;
+import java.util.List;
+import java.util.Map;
+
+import static java.util.Collections.emptyList;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static travelator.money.Currencies.*;
+
+
+public class RouteCostTest {
+ ExchangeRates fx =
+ new ExchangeRatesViaBaseCurrency(
+ EUR,
+ Map.entry(GBP, new BigDecimal("0.8")),
+ Map.entry(USD, new BigDecimal("1.6")),
+ Map.entry(JOD, new BigDecimal("100"))
+ );
+ Currency userCurrency =
+ GBP;
+
+ CostSummaryCalculator calculator =
+ new CostSummaryCalculator(userCurrency, fx); // <1>
+
+ public CostSummary costSummary(Route r) {
+ r.addCostsTo(calculator); // <2>
+ return calculator.summarise(); // <3>
+ }
+
+ @Test
+ public void an_empty_route_costs_nothing() throws IOException {
+ var r = new Route(emptyList());
+
+ var costs = costSummary(r);
+
+ assertEquals(Money.zero(GBP), costs.getTotal());
+ }
+
+ @Test
+ public void route_in_local_currency() throws IOException {
+ var r = new Route(List.of(
+ journeyCosting(Money.of("80", GBP)),
+ journeyCosting(Money.of("35", GBP))));
+
+ var costs = costSummary(r);
+
+ assertEquals(Money.of("115", GBP), costs.getTotal());
+ }
+
+ @Test
+ public void route_in_one_foreign_currency() throws IOException {
+ var r = new Route(List.of(
+ journeyCosting(Money.of("100", EUR)),
+ journeyCosting(Money.of("50", EUR))));
+
+ var costs = costSummary(r);
+
+ assertEquals(Money.of("120", GBP), costs.getTotal());
+ }
+
+ @Test
+ public void route_in_foreign_and_local_currency() throws IOException {
+ var r = new Route(List.of(
+ journeyCosting(Money.of("100", EUR)),
+ journeyCosting(Money.of("50", GBP))));
+
+ var costs = costSummary(r);
+
+ assertEquals(Money.of("130", GBP), costs.getTotal());
+ }
+
+ @Test
+ public void route_in_multiple_foreign_and_local_currencies() throws IOException {
+ var r = new Route(List.of(
+ journeyCosting(Money.of("250", EUR)),
+ journeyCosting(Money.of("10000", JOD)),
+ journeyCosting(Money.of("750", GBP))));
+
+ var costs = costSummary(r);
+
+ assertEquals(Money.of("1030", GBP), costs.getTotal());
+ }
+
+ private Journey journeyCosting(Money cost) {
+ Journey j = new Journey();
+ j.setPrice(cost);
+ return j;
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/travelator/itinerary/RouteRequiredAccommodationTest.java b/src/test/java/travelator/itinerary/RouteRequiredAccommodationTest.java
new file mode 100644
index 0000000..d379de7
--- /dev/null
+++ b/src/test/java/travelator/itinerary/RouteRequiredAccommodationTest.java
@@ -0,0 +1,122 @@
+package travelator.itinerary;
+
+import org.junit.jupiter.api.Test;
+import travelator.Id;
+import travelator.Location;
+import travelator.money.Money;
+
+import java.util.List;
+
+import static java.util.Collections.emptyList;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static travelator.itinerary.TestData.*;
+import static travelator.money.Currencies.EUR;
+
+public class RouteRequiredAccommodationTest {
+ @Test
+ public void an_empty_route_does_not_need_accommodation() {
+ Route emptyRoute = new Route(emptyList());
+
+ assertEquals(emptyList(), emptyRoute.requiredAccommodation());
+ }
+
+ @Test
+ public void a_route_of_one_journey_does_not_need_accommodation() {
+ Route r = routeOf(
+ journey(
+ Jakarta_Tanjung_Priok_Ferry_Terminal, "2020-08-06 23:59 Asia/Jakarta",
+ Batam_Batu_Ampar_Ferry_Terminal, "2020-08-08 05:40 Asia/Jakarta"));
+ assertEquals(emptyList(), r.requiredAccommodation());
+ }
+
+ @Test
+ public void a_route_of_two_journeys_does_not_need_accommodation_if_continue_on_same_day() {
+ Route r = routeOf(
+ journey(
+ Batam_Sekupang_Ferry_Terminal, "2020-08-08 07:00 Asia/Jakarta",
+ Singapore_Harbourfront_Centre, "2020-08-08 06:50 Asia/Singapore"),
+ journey(
+ Singapore_Harbourfront_Centre, "2020-08-08 07:00 Asia/Singapore",
+ Johor_Bahru_Sentral, "2020-08-08 08:00 Asia/Kuala_Lumpur"));
+
+ assertEquals(emptyList(), r.requiredAccommodation());
+ }
+
+ @Test
+ public void a_route_of_two_journeys_that_requires_accommodation() {
+ Route r = routeOf(
+ journey(
+ Johor_Bahru_Sentral, "2020-08-10 10:10 Asia/Kuala_Lumpur",
+ Kuala_Lumpur_Sentral, "2020-08-10 17:36 Asia/Kuala_Lumpur"),
+ journey(
+ Kuala_Lumpur_Sentral, "2020-08-11 10:28 Asia/Kuala_Lumpur",
+ Bangkok, "2020-08-12 09:10 Asia/Bangkok"));
+
+ assertEquals(
+ List.of(new Interchange(
+ Kuala_Lumpur_Sentral, dateTime("2020-08-10 17:36 Asia/Kuala_Lumpur"),
+ Kuala_Lumpur_Sentral, dateTime("2020-08-11 10:28 Asia/Kuala_Lumpur"))),
+ r.requiredAccommodation());
+ }
+
+ @Test
+ public void a_route_of_two_journeys_that_has_a_short_overnight_wait_so_does_not_need_accommodation() {
+ Route r = routeOf(
+ journey(
+ Johor_Bahru_Sentral, "2020-08-10 10:10 Asia/Kuala_Lumpur",
+ Kuala_Lumpur_Sentral, "2020-08-10 23:30 Asia/Kuala_Lumpur"),
+ journey(
+ Kuala_Lumpur_Sentral, "2020-08-11 00:30 Asia/Kuala_Lumpur",
+ Bangkok, "2020-08-12 09:10 Asia/Bangkok"));
+
+ assertEquals(emptyList(), r.requiredAccommodation());
+ }
+
+ @Test
+ public void a_route_that_requires_accommodation_for_multiple_nights() {
+ Route r = routeOf(
+ journey(
+ Jakarta_Tanjung_Priok_Ferry_Terminal, "2020-08-06 23:59 Asia/Jakarta",
+ Batam_Batu_Ampar_Ferry_Terminal, "2020-08-08 05:40 Asia/Jakarta"),
+ journey(
+ Batam_Sekupang_Ferry_Terminal, "2020-08-08 07:00 Asia/Jakarta",
+ Singapore_Harbourfront_Centre, "2020-08-08 06:50 Asia/Singapore"),
+ journey(
+ Johor_Bahru_Sentral, "2020-08-10 10:10 Asia/Kuala_Lumpur",
+ Kuala_Lumpur_Sentral, "2020-08-10 17:36 Asia/Kuala_Lumpur"),
+ journey(
+ Kuala_Lumpur_Sentral, "2020-08-11 10:28 Asia/Kuala_Lumpur",
+ Bangkok, "2020-08-12 09:10 Asia/Bangkok"));
+
+ assertEquals(
+ List.of(
+ new Interchange(
+ Singapore_Harbourfront_Centre, dateTime("2020-08-08 06:50 Asia/Singapore"),
+ Johor_Bahru_Sentral, dateTime("2020-08-10 10:10 Asia/Kuala_Lumpur")),
+ new Interchange(
+ Kuala_Lumpur_Sentral, dateTime("2020-08-10 17:36 Asia/Kuala_Lumpur"),
+ Kuala_Lumpur_Sentral, dateTime("2020-08-11 10:28 Asia/Kuala_Lumpur"))),
+ r.requiredAccommodation());
+ }
+
+ private Journey journey(Location origin,
+ String departureTimeStr,
+ Location destination,
+ String arrivalTimeStr
+ ) {
+ return new Journey(
+ origin,
+ destination,
+ Id.of("example-operator"),
+ TravelMethod.RAIL,
+ dateTime(departureTimeStr),
+ dateTime(arrivalTimeStr),
+ "Standard",
+ Money.of(100, EUR)
+ );
+ }
+
+ public static Route routeOf(Journey... journeys) {
+ return new Route(List.of(journeys));
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/travelator/itinerary/TestData.java b/src/test/java/travelator/itinerary/TestData.java
new file mode 100644
index 0000000..313d773
--- /dev/null
+++ b/src/test/java/travelator/itinerary/TestData.java
@@ -0,0 +1,63 @@
+package travelator.itinerary;
+
+import travelator.Id;
+import travelator.Location;
+
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeFormatterBuilder;
+
+import static java.time.format.DateTimeFormatter.ISO_LOCAL_DATE;
+import static java.time.format.DateTimeFormatter.ISO_LOCAL_TIME;
+import static java.util.Locale.ROOT;
+
+public class TestData {
+ public static Location Jakarta_Tanjung_Priok_Ferry_Terminal = new Location(
+ Id.of("TestData.Jakarta_Tanjung_Priok_Ferry_Terminal"),
+ "Jakarta Tanjung Priok",
+ "Jakarta Tanjung Priok");
+
+ public static Location Batam_Batu_Ampar_Ferry_Terminal = new Location(
+ Id.of("TestData.Batam_Batu_Ampar_Ferry_Terminal"),
+ "Batam Batu Ampar",
+ "Batam Batu Ampar");
+
+ public static Location Batam_Sekupang_Ferry_Terminal = new Location(
+ Id.of("TestData.Batam_Sekupang_Ferry_Terminal"),
+ "Batam Sekupang",
+ "Batam Sekupang");
+
+ public static Location Singapore_Harbourfront_Centre = new Location(
+ Id.of("TestData.Singapore_Harbourfront_Centre"),
+ "Singapore Harbourfront Centre",
+ "Singapore Harbourfront Centre");
+
+ public static Location Johor_Bahru_Sentral = new Location(
+ Id.of("TestData.Johor_Bahru_Sentral"),
+ "Johor Bahru Sentral",
+ "Johor Bahru Central Station");
+
+ public static Location Kuala_Lumpur_Sentral = new Location(
+ Id.of("TestData.Kuala_Lumpur_Sentral"),
+ "Kuala Lumpur Sentral",
+ "Kuala Lumpur Central Station");
+
+ public static Location Bangkok = new Location(
+ Id.of("TestData.Bangkok"),
+ "Bangkok",
+ "Bangkok");
+
+ public static ZonedDateTime dateTime(String s) {
+ return ZonedDateTime.parse(s, formatter);
+ }
+
+ private static final DateTimeFormatter formatter = new DateTimeFormatterBuilder()
+ .parseCaseInsensitive()
+ .parseCaseInsensitive()
+ .append(ISO_LOCAL_DATE)
+ .appendLiteral(" ")
+ .append(ISO_LOCAL_TIME)
+ .appendLiteral(" ")
+ .appendZoneOrOffsetId()
+ .toFormatter(ROOT);
+}
\ No newline at end of file
diff --git a/src/test/java/travelator/money/Currencies.java b/src/test/java/travelator/money/Currencies.java
new file mode 100644
index 0000000..fe7b199
--- /dev/null
+++ b/src/test/java/travelator/money/Currencies.java
@@ -0,0 +1,16 @@
+package travelator.money;
+
+import java.util.Currency;
+
+public class Currencies {
+ // These all have 100 minor units per major unit
+ public static final Currency GBP = Currency.getInstance("GBP");
+ public static final Currency USD = Currency.getInstance("USD");
+ public static final Currency EUR = Currency.getInstance("EUR");
+
+ // JOD has 1000 minor units per major unit
+ public static final Currency JOD = Currency.getInstance("JOD");
+
+ // JPY has no minor units
+ public static final Currency JPY = Currency.getInstance("JPY");
+}
\ No newline at end of file
diff --git a/src/test/java/travelator/money/ExchangeRatesViaBaseCurrencyTest.java b/src/test/java/travelator/money/ExchangeRatesViaBaseCurrencyTest.java
new file mode 100644
index 0000000..96699d3
--- /dev/null
+++ b/src/test/java/travelator/money/ExchangeRatesViaBaseCurrencyTest.java
@@ -0,0 +1,47 @@
+package travelator.money;
+
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static travelator.money.Currencies.*;
+
+public class ExchangeRatesViaBaseCurrencyTest {
+ private final ExchangeRates fx = new ExchangeRatesViaBaseCurrency(
+ EUR,
+ Map.entry(GBP, new BigDecimal("0.8")),
+ Map.entry(JOD, new BigDecimal("100"))
+ );
+
+ @Test
+ public void same_currency() throws IOException {
+ assertEquals(
+ Money.of("125", EUR),
+ fx.convert(Money.of("125", EUR), EUR).getToMoney());
+ }
+
+ @Test
+ public void one_foreign_currency() throws IOException {
+ assertConversionBothWays(Money.of("100", EUR), Money.of("80", GBP));
+ }
+
+ @Test
+ public void two_foreign_currencies() throws IOException {
+ assertConversionBothWays(Money.of("80", GBP), Money.of("10000", JOD));
+ }
+
+ public void assertConversionBothWays(Money m1, Money m2) throws IOException {
+ assertConversion(m1, m2);
+ assertConversion(m2, m1);
+ }
+
+ public void assertConversion(Money fromMoney, Money toMoney) throws IOException {
+ assertEquals(
+ toMoney,
+ fx.convert(fromMoney, toMoney.getCurrency()).getToMoney(),
+ () -> fromMoney.getCurrency() + " -> " + toMoney.getCurrency());
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/travelator/money/MoneyTest.kt b/src/test/java/travelator/money/MoneyTest.kt
new file mode 100644
index 0000000..b835713
--- /dev/null
+++ b/src/test/java/travelator/money/MoneyTest.kt
@@ -0,0 +1,35 @@
+package travelator.money
+
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Assertions.assertThrows
+import org.junit.jupiter.api.Test
+import travelator.money.Currencies.*
+
+class MoneyTest {
+ @Test
+ fun representation() {
+ assertEquals("17.25 GBP", Money.of("17.25", GBP).toString())
+ assertEquals("20.00 EUR", Money.of("20", EUR).toString())
+ assertEquals("1.500 JOD", Money.of("1.5", JOD).toString())
+ assertEquals("1200 JPY", Money.of("1200", JPY).toString())
+ }
+
+ @Test
+ fun adding() {
+ assertEquals(
+ Money(275, EUR),
+ Money(200, EUR) + Money(75, EUR)
+ )
+ assertEquals(
+ Money(266, GBP),
+ Money(266, GBP) + Money.zero(GBP)
+ )
+ }
+
+ @Test
+ fun cannot_add_money_of_different_currencies() {
+ assertThrows(IllegalArgumentException::class.java) {
+ Money(1, GBP) + Money(2, EUR)
+ }
+ }
+}
\ No newline at end of file