diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..040b81a --- /dev/null +++ b/.gitignore @@ -0,0 +1,36 @@ +### Gradle files ### +.gradle/ +build/ +!gradle-wrapper.jar +!gradle-wrapper.properties + +### IntelliJ IDEA files ### +.idea/ +*.iml +*.iws +*.ipr + +### OS-specific files ### +.DS_Store +Thumbs.db + +### Log files ### +*.log + +### Temporary files ### +*.swp +*.swo +*.bak + +### Gradle Wrapper ### +gradle-wrapper.jar +gradle-wrapper.properties + +### Compiled class files ### +*.class + +### JetBrains Rider ### +.idea/.idea_modules/ + +### IntelliJ project files ### +out/ diff --git a/build.gradle b/build.gradle index efba2e7..1ca35a1 100644 --- a/build.gradle +++ b/build.gradle @@ -7,13 +7,12 @@ apply plugin: 'eclipse' version = '1.0-SNAPSHOT' group = 'org.suggs.interviews.berlinclock' -task wrapper(type: Wrapper){ +wrapper { description = 'Generates gradlew scripts for NIX and win envs' - gradleVersion = '2.0' + gradleVersion = '7.3' } repositories { - jcenter() mavenCentral() } @@ -22,15 +21,18 @@ idea.module { } dependencies { - compile 'org.slf4j:slf4j-api:1.7.5', + implementation 'org.slf4j:slf4j-api:1.7.5', 'commons-lang:commons-lang:2.6' - runtime 'org.slf4j:slf4j-log4j12:1.7.5', + implementation 'org.slf4j:slf4j-log4j12:1.7.5', 'log4j:log4j:1.2.17' - testCompile 'junit:junit:4.11', - 'org.mockito:mockito-core:1.9.5', - 'org.assertj:assertj-core:1.6.1', - 'commons-io:commons-io:2.4', - 'org.jbehave:jbehave-core:3.8' + testImplementation 'org.mockito:mockito-core:5.15.2', + 'org.assertj:assertj-core:3.27.3', + 'commons-io:commons-io:2.18.0', + 'org.jbehave:jbehave-core:5.2.0' + // Jupiter api and engine dependencies + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.3' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.3' + } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 6a0e65d..2addbc9 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-bin.zip diff --git a/instructions/build.gradle b/instructions/build.gradle index 5a3009b..b77d5e6 100644 --- a/instructions/build.gradle +++ b/instructions/build.gradle @@ -7,13 +7,13 @@ apply plugin: 'eclipse' version = '1.0-SNAPSHOT' group = 'org.suggs.interviews.example' -task wrapper(type: Wrapper){ +wrapper { description = 'Generates gradlew scripts for NIX and win envs' - gradleVersion = '2.0' + gradleVersion = '7.3' } repositories { - jcenter() + mavenCentral() mavenLocal() } @@ -22,10 +22,10 @@ idea.module { } dependencies { - compile 'org.slf4j:slf4j-api:1.7.5' + implementation 'org.slf4j:slf4j-api:1.7.5' - runtime 'org.slf4j:slf4j-log4j12:1.7.5', + implementation 'org.slf4j:slf4j-log4j12:1.7.5', 'log4j:log4j:1.2.17' - testCompile 'junit:junit:4.11' + testImplementation 'junit:junit:4.11' } diff --git a/instructions/gradle/wrapper/gradle-wrapper.properties b/instructions/gradle/wrapper/gradle-wrapper.properties index 74e3e0c..e77ad86 100644 --- a/instructions/gradle/wrapper/gradle-wrapper.properties +++ b/instructions/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-bin.zip diff --git a/instructions/src/test/resources/log4j.xml b/instructions/src/test/resources/log4j.xml index 9402a1c..0478763 100644 --- a/instructions/src/test/resources/log4j.xml +++ b/instructions/src/test/resources/log4j.xml @@ -1,5 +1,5 @@ - + diff --git a/src/main/java/com/ubs/opsit/interviews/BerlinClock.java b/src/main/java/com/ubs/opsit/interviews/BerlinClock.java new file mode 100644 index 0000000..858d55f --- /dev/null +++ b/src/main/java/com/ubs/opsit/interviews/BerlinClock.java @@ -0,0 +1,89 @@ +package com.ubs.opsit.interviews; + +import java.util.Objects; + +/** + * @author Pankaj + */ +public class BerlinClock implements TimeConverter { + @Override + public String convertTime(String time) { + if (time == null || time.isEmpty()) { + throw new IllegalArgumentException("Invalid time format"); + } + + // Handle invalid time format + String[] values = time.split(":"); + if (values.length != 3) { + throw new IllegalArgumentException("Invalid time format"); + } + + try { + int hours = Integer.parseInt(values[0]); + int minutes = Integer.parseInt(values[1]); + int seconds = Integer.parseInt(values[2]); + + // Validate hours, minutes, and seconds range + if (hours < 0 || hours > 24 || minutes < 0 || minutes > 59 || seconds < 0 || seconds > 59) { + throw new IllegalArgumentException("Invalid time format"); + } + + // If hours == 24, check if minutes and seconds are 00 + if (hours == 24 && (minutes != 0 || seconds != 0)) { + throw new IllegalArgumentException("Invalid time format"); + } + + // Construct the Berlin Clock string + return getLampOnOff(seconds) + " " + getHours(hours) + " " + getMinutes(minutes); + + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid time format"); + } + } + + + /** + * Every 2 seconds lamp 1st row blinks on/off + * @param seconds + * @return + */ + protected String getLampOnOff(int seconds) { + return seconds % 2 == 0 ? LampSymbol.Y.name() : LampSymbol.O.name(); + } + + protected String getHours(int hours) { + int numberTopHourLamps = hours / 5; + int numberBottomHourLamps = hours % 5; + + return getLampRow(4, numberTopHourLamps, LampSymbol.R) + " " + getLampRow(4, numberBottomHourLamps, LampSymbol.R); + } + + protected String getMinutes(int minutes) { + int numberTopMinutesLamps = minutes / 5; + int numberBottomMinutesLamps = minutes % 5; + + StringBuilder sb = new StringBuilder(); + + for (int i = 1; i <= 11; i++) { + sb.append(i <= numberTopMinutesLamps ? getMinuteLampColour(i) : LampSymbol.O.name()); + } + + sb.append(" "); + + sb.append(getLampRow(4, numberBottomMinutesLamps, LampSymbol.Y)); + + return sb.toString(); + } + + private String getLampRow(int totalNumberLamps, int numberLampsOn, LampSymbol lampSymbol) { + StringBuilder sb = new StringBuilder(totalNumberLamps); + for (int i = 0; i < totalNumberLamps; i++) { + sb.append(i < numberLampsOn ? lampSymbol : LampSymbol.O.name()); + } + return sb.toString(); + } + + private String getMinuteLampColour(int index) { + return index % 3 == 0 ? LampSymbol.R.name() : LampSymbol.Y.name(); + } +} diff --git a/src/main/java/com/ubs/opsit/interviews/BerlinClockMain.java b/src/main/java/com/ubs/opsit/interviews/BerlinClockMain.java new file mode 100644 index 0000000..3346a72 --- /dev/null +++ b/src/main/java/com/ubs/opsit/interviews/BerlinClockMain.java @@ -0,0 +1,13 @@ +package com.ubs.opsit.interviews; + +public class BerlinClockMain { + + public static void main(String[] args) { + BerlinClock clock = new BerlinClock(); + //clock.convertTime(""); + //clock.convertTime("23.59.59"); + String result = clock.convertTime("23:59:59"); + System.out.println(result); + + } +} diff --git a/src/main/java/com/ubs/opsit/interviews/LampSymbol.java b/src/main/java/com/ubs/opsit/interviews/LampSymbol.java new file mode 100644 index 0000000..f768b70 --- /dev/null +++ b/src/main/java/com/ubs/opsit/interviews/LampSymbol.java @@ -0,0 +1,10 @@ +package com.ubs.opsit.interviews; + +/** + * @author Pankaj + */ +public enum LampSymbol { + O, + Y, + R; +} diff --git a/src/test/java/com/ubs/opsit/interviews/BerlinClockFixture.java b/src/test/java/com/ubs/opsit/interviews/BerlinClockFixture.java index 0310f71..d6beb8d 100644 --- a/src/test/java/com/ubs/opsit/interviews/BerlinClockFixture.java +++ b/src/test/java/com/ubs/opsit/interviews/BerlinClockFixture.java @@ -2,10 +2,10 @@ import org.jbehave.core.annotations.Then; import org.jbehave.core.annotations.When; -import org.junit.Test; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; import static com.ubs.opsit.interviews.support.BehaviouralTestEmbedder.aBehaviouralTestRunner; -import static org.assertj.core.api.Assertions.assertThat; /** * Acceptance test class that uses the JBehave (Gerkin) syntax for writing stories. You should not need to @@ -13,14 +13,14 @@ */ public class BerlinClockFixture { - private TimeConverter berlinClock; + private TimeConverter berlinClock = new BerlinClock(); // Initialize BerlinClock private String theTime; @Test public void berlinClockAcceptanceTests() throws Exception { aBehaviouralTestRunner() .usingStepsFrom(this) - .withStory("berlin-clock.story") + .withStory("berlin-clock.story") // The story file that contains scenarios .run(); } @@ -31,6 +31,21 @@ public void whenTheTimeIs(String time) { @Then("the clock should look like $") public void thenTheClockShouldLookLike(String theExpectedBerlinClockOutput) { - assertThat(berlinClock.convertTime(theTime)).isEqualTo(theExpectedBerlinClockOutput); + try { + String result = berlinClock.convertTime(theTime); + Assertions.assertEquals(theExpectedBerlinClockOutput, result); + } catch (IllegalArgumentException e) { + Assertions.assertEquals("Invalid time format", e.getMessage()); + } + } + + @Then("the clock should throw an error $") + public void thenTheClockShouldThrowAnError(String expectedError) { + try { + berlinClock.convertTime(theTime); + Assertions.fail("Expected error not thrown"); + } catch (IllegalArgumentException e) { + Assertions.assertEquals(expectedError, e.getMessage()); + } } } diff --git a/src/test/java/com/ubs/opsit/interviews/support/BehaviouralTestEmbedder.java b/src/test/java/com/ubs/opsit/interviews/support/BehaviouralTestEmbedder.java index c5abeba..ecf850d 100644 --- a/src/test/java/com/ubs/opsit/interviews/support/BehaviouralTestEmbedder.java +++ b/src/test/java/com/ubs/opsit/interviews/support/BehaviouralTestEmbedder.java @@ -9,13 +9,13 @@ import org.jbehave.core.steps.InjectableStepsFactory; import org.jbehave.core.steps.InstanceStepsFactory; import org.jbehave.core.steps.ParameterConverters; +import org.junit.jupiter.api.Assertions; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.text.SimpleDateFormat; import java.util.List; -import static org.assertj.core.api.Assertions.assertThat; import static org.jbehave.core.io.CodeLocations.codeLocationFromClass; import static org.jbehave.core.reporters.Format.CONSOLE; import static org.jbehave.core.reporters.Format.HTML; @@ -42,7 +42,7 @@ public static BehaviouralTestEmbedder aBehaviouralTestRunner() { } @Override - public void run() throws Exception { + public void run() { List paths = createStoryPaths(); if (paths == null || paths.isEmpty()) { throw new IllegalStateException("No story paths found for state machine"); @@ -53,7 +53,7 @@ public void run() throws Exception { @Override public InjectableStepsFactory stepsFactory() { - assertThat(stepsFactory).isNotNull(); + Assertions.assertNotNull(stepsFactory); return stepsFactory; } @@ -74,7 +74,7 @@ public BehaviouralTestEmbedder withStory(String aWildcardStoryFilename) { } public BehaviouralTestEmbedder usingStepsFrom(Object... stepsSource) { - assertThat(stepsFactory).isNull(); + Assertions.assertNull(stepsFactory); stepsFactory = new InstanceStepsFactory(configuration(), stepsSource); return this; } diff --git a/src/test/java/com/ubs/opsit/interviews/support/ClasspathStoryFinder.java b/src/test/java/com/ubs/opsit/interviews/support/ClasspathStoryFinder.java index e74713a..133e553 100644 --- a/src/test/java/com/ubs/opsit/interviews/support/ClasspathStoryFinder.java +++ b/src/test/java/com/ubs/opsit/interviews/support/ClasspathStoryFinder.java @@ -1,7 +1,7 @@ package com.ubs.opsit.interviews.support; import org.apache.commons.io.filefilter.DirectoryFileFilter; -import org.apache.commons.io.filefilter.WildcardFileFilter; +import org.apache.commons.io.filefilter.RegexFileFilter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -23,7 +23,7 @@ public final class ClasspathStoryFinder { private static final Logger LOG = LoggerFactory.getLogger(ClasspathStoryFinder.class); public static List findFilenamesThatMatch(String aFilenameWithWildcards) { - List filenames = new ArrayList(); + List filenames = new ArrayList<>(); for (File file : findFilesThatMatch(aFilenameWithWildcards)) { filenames.add(file.toURI().toString()); } @@ -31,11 +31,11 @@ public static List findFilenamesThatMatch(String aFilenameWithWildcards) } private static Collection findFilesThatMatch(String aFilenameWithWildcards) { - WildcardFileFilter regexFileFilter = new WildcardFileFilter(aFilenameWithWildcards); + RegexFileFilter regexFileFilter = new RegexFileFilter(aFilenameWithWildcards); List rootDirsToSearchFrom = getRootDirs(); LOG.info("Searching for stories called [{}] in [{}]", aFilenameWithWildcards, rootDirsToSearchFrom); - List ret = new ArrayList() ; + List ret = new ArrayList<>() ; for (File f : rootDirsToSearchFrom) { ret.addAll(listFiles(f, regexFileFilter, DirectoryFileFilter.DIRECTORY)) ; } @@ -43,7 +43,7 @@ private static Collection findFilesThatMatch(String aFilenameWithWildcards } private static List getRootDirs() { - List ret = new ArrayList() ; + List ret = new ArrayList<>() ; try { Enumeration roots = ClasspathStoryFinder.class.getClassLoader().getResources("") ; while(roots.hasMoreElements()) { diff --git a/src/test/resources/log4j.xml b/src/test/resources/log4j.xml index cbf5bd8..42ca570 100644 --- a/src/test/resources/log4j.xml +++ b/src/test/resources/log4j.xml @@ -1,5 +1,5 @@ - + diff --git a/src/test/resources/stories/berlin-clock.story b/src/test/resources/stories/berlin-clock.story index 7d64304..8d4d15d 100644 --- a/src/test/resources/stories/berlin-clock.story +++ b/src/test/resources/stories/berlin-clock.story @@ -44,5 +44,33 @@ RRRR OOOOOOOOOOO OOOO +Scenario: Null time + When the time is null + Then the clock should throw an error "Invalid time format" + +Scenario: Empty time value + When the time is "" + Then the clock should throw an error "Invalid time format" + +Scenario: Invalid time format (with dots instead of colons) + When the time is 23.34.34 + Then the clock should throw an error "Invalid time format" + +Scenario: Invalid hour value (greater than 24) + When the time is 25:30:45 + Then the clock should throw an error "Invalid time format" + +Scenario: Invalid minute value (greater than 59) + When the time is 12:60:30 + Then the clock should throw an error "Invalid time format" + +Scenario: Invalid second value (greater than 59) + When the time is 12:30:60 + Then the clock should throw an error "Invalid time format" + +Scenario: Invalid time format (non-numeric value) + When the time is "abc:def:ghi" + Then the clock should throw an error "Invalid time format" +