diff --git a/src/main/java/reactivestreams/ColumnChecker.java b/src/main/java/reactivestreams/ColumnChecker.java new file mode 100644 index 0000000..4df362b --- /dev/null +++ b/src/main/java/reactivestreams/ColumnChecker.java @@ -0,0 +1,16 @@ +package reactivestreams; + +public class ColumnChecker extends DigitBlockChecker { + + int column; + + public ColumnChecker(int column) { + this.column = column; + } + + @Override + protected boolean currentDigitApplies(int currentRow, int currentColumn) { + return currentColumn == column; + } + +} diff --git a/src/main/java/reactivestreams/DigitBlockChecker.java b/src/main/java/reactivestreams/DigitBlockChecker.java new file mode 100644 index 0000000..3a151da --- /dev/null +++ b/src/main/java/reactivestreams/DigitBlockChecker.java @@ -0,0 +1,58 @@ +package reactivestreams; + +import java.util.Collection; +import java.util.HashSet; +import java.util.concurrent.Flow; +import java.util.concurrent.SubmissionPublisher; + +/** + * Processor consuming a flow of digit Strings and emitting a single Boolean which is true if applying 9 digits are unique. + */ +public abstract class DigitBlockChecker extends SubmissionPublisher implements Flow.Processor { + + int currentRow; + int currentColumn; + Collection digitsProcessed; + + @Override + public void onSubscribe(Flow.Subscription subscription) { + currentRow = 1; + currentColumn = 0; + digitsProcessed = new HashSet<>(); + subscription.request(9*9); + } + + @Override + public void onNext(String digit) { + currentColumn++; + if (currentColumn > 9) { + currentRow++; + currentColumn = 1; + } + if (currentDigitApplies(currentRow, currentColumn)) { + if (digitsProcessed.contains(digit)) { + submit(false); + close(); + } + digitsProcessed.add(digit); + if (digitsProcessed.size() == 9) { + submit(true); + close(); + } + } + } + + protected abstract boolean currentDigitApplies(int currentRow, int currentColumn); + + @Override + public void onError(Throwable throwable) { + System.err.println(throwable.fillInStackTrace()); + close(); + } + + @Override + public void onComplete() { + close(); + } + +} diff --git a/src/main/java/reactivestreams/RowChecker.java b/src/main/java/reactivestreams/RowChecker.java new file mode 100644 index 0000000..7212ef6 --- /dev/null +++ b/src/main/java/reactivestreams/RowChecker.java @@ -0,0 +1,16 @@ +package reactivestreams; + +public class RowChecker extends DigitBlockChecker { + + int row; + + public RowChecker(int row) { + this.row = row; + } + + @Override + protected boolean currentDigitApplies(int currentRow, int currentColumn) { + return currentRow == row; + } + +} diff --git a/src/main/java/reactivestreams/Solution.java b/src/main/java/reactivestreams/Solution.java new file mode 100644 index 0000000..b1fc363 --- /dev/null +++ b/src/main/java/reactivestreams/Solution.java @@ -0,0 +1,24 @@ +package reactivestreams; + +import java.util.Scanner; +import java.util.Spliterator; +import java.util.concurrent.SubmissionPublisher; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +/** + * stream.Solution involving a Sukdoku stream.Board, a stream.SudokuVerifier, plus some ReactiveStreams processing. +**/ +class Solution { + + public static void main( String[] args ) { + // Setup stream on System.in + Scanner in = new Scanner( System.in ); + Spliterator spliterator = ((Iterable) () -> in).spliterator(); + Stream digitStream = StreamSupport.stream(spliterator, false); + // Setup SudokuVerifier + SudokuVerifier verifier = new SudokuVerifier(digitStream); + // Verify and print out result + System.out.println(verifier.isSolved()); + } +} diff --git a/src/main/java/reactivestreams/SquareChecker.java b/src/main/java/reactivestreams/SquareChecker.java new file mode 100644 index 0000000..4580e25 --- /dev/null +++ b/src/main/java/reactivestreams/SquareChecker.java @@ -0,0 +1,30 @@ +package reactivestreams; + +public class SquareChecker extends DigitBlockChecker { + + int squareRow; + int squareColumn; + + public SquareChecker(int squareRow, int squareColumn) { + this.squareRow = squareRow; + this.squareColumn = squareColumn; + } + + @Override + protected boolean currentDigitApplies(int currentRow, int currentColumn) { + return currentRowApplies(currentRow) && currentColumnApplies(currentColumn); + } + + boolean currentRowApplies(int currentRow) { + int fromRow = squareRow * 3 - 2; + int toRow = fromRow + 2; + return currentRow >= fromRow && currentRow <= toRow; + } + + boolean currentColumnApplies(int currentColumn) { + int fromColumn = squareColumn * 3 - 2; + int toColumn = fromColumn + 2; + return currentColumn >= fromColumn && currentColumn <= toColumn; + } + +} diff --git a/src/main/java/reactivestreams/SudokuVerifier.java b/src/main/java/reactivestreams/SudokuVerifier.java new file mode 100644 index 0000000..3208e06 --- /dev/null +++ b/src/main/java/reactivestreams/SudokuVerifier.java @@ -0,0 +1,80 @@ +package reactivestreams; + +import java.util.concurrent.Flow; +import java.util.concurrent.SubmissionPublisher; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +public class SudokuVerifier implements Flow.Subscriber { + + Stream digitStream; + SubmissionPublisher digitPublisher; + AtomicInteger finished = new AtomicInteger(0); + AtomicBoolean solved = new AtomicBoolean(true); + + public SudokuVerifier(Stream digitStream) { + this.digitStream = digitStream; + // Setup publisher to emit the digitStream + digitPublisher = new SubmissionPublisher<>(); + // Setup row and column checking subscribers + IntStream.rangeClosed(1, 9).boxed() + .forEach(i -> rowCheckerAndColumnChecker(digitPublisher, i)); + // Setup square checking subscribers + IntStream.rangeClosed(1, 3).boxed() + .forEach(squareRow -> { + IntStream.rangeClosed(1, 3).boxed() + .forEach(squareColumn -> subscribeBlockChecker(digitPublisher, new SquareChecker(squareRow, squareColumn))); + }); + } + + public boolean isSolved() { + // Make publisher emit each digit + digitStream.forEach(digitPublisher::submit); + digitPublisher.close(); + // Wait for checking subscribers to finish + while (finished.get() < digitPublisher.getSubscribers().size() && solved.get()) { + try { + Thread.sleep(5); + } catch (InterruptedException ex) { + System.out.println(ex.fillInStackTrace()); + } + } + return solved.get(); + } + + private void rowCheckerAndColumnChecker(SubmissionPublisher digitPublisher, Integer i) { + subscribeBlockChecker(digitPublisher, new RowChecker(i)); + subscribeBlockChecker(digitPublisher, new ColumnChecker(i)); + } + + private void subscribeBlockChecker(SubmissionPublisher digitPublisher, DigitBlockChecker blockChecker) { + digitPublisher.subscribe(blockChecker); + blockChecker.subscribe(this); + } + + @Override + public void onSubscribe(Flow.Subscription subscription) { + subscription.request(1); // 1 Boolean result from the block checker + } + + @Override + public void onNext(Boolean solved) { + if (! solved) { + this.solved.compareAndSet(true, false); + } + } + + @Override + public void onError(Throwable throwable) { + System.err.println(throwable.fillInStackTrace()); + solved.compareAndSet(true, false); + finished.incrementAndGet(); + } + + @Override + public void onComplete() { + finished.incrementAndGet(); + } +} diff --git a/src/test/java/reactivestreams/SolutionTest.java b/src/test/java/reactivestreams/SolutionTest.java new file mode 100644 index 0000000..9763fc6 --- /dev/null +++ b/src/test/java/reactivestreams/SolutionTest.java @@ -0,0 +1,92 @@ +package reactivestreams; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.converter.ArgumentConversionException; +import org.junit.jupiter.params.converter.ConvertWith; +import org.junit.jupiter.params.converter.SimpleArgumentConverter; +import org.junit.jupiter.params.provider.CsvFileSource; + +import java.io.*; +import java.util.stream.Collectors; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class SolutionTest { + + static class SudokuConverter extends SimpleArgumentConverter { + @Override + protected Object convert( Object source, Class targetType ) throws ArgumentConversionException { + assertEquals(String.class, targetType, "Can only convert to String"); + BufferedReader in = new BufferedReader( new InputStreamReader( SolutionTest.class.getResourceAsStream( String.valueOf( source )))); + return in.lines().collect( Collectors.joining( System.lineSeparator() )); + } + } + + static long totalDuration; + + @BeforeAll + static void beforeAll() { + totalDuration = 0; + } + + @AfterAll + static void afterAll() { + System.out.println( String.format( "All solutions took %01d.%03d secs", totalDuration / 1000, totalDuration % 1000 )); + } + + @ParameterizedTest + @CsvFileSource( resources = "/testdata.csv" ) + void main( @ConvertWith( SudokuConverter.class ) final String input, final String expected ) throws IOException { + // keep original streams + InputStream oldIn = System.in; + PrintStream oldOut = System.out; + PrintStream oldErr = System.err; + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + + try { + redirectStreams( + new ByteArrayInputStream( input.getBytes( UTF_8 )), + new PrintStream( bos, true, UTF_8 ), + new PrintStream( new ByteArrayOutputStream(), true, UTF_8 ) + ); + + // start time tracking + long start = System.currentTimeMillis(); + + Solution.main( new String[0] ); + + // stop time tracking + long duration = System.currentTimeMillis() - start; + totalDuration += duration; + + // restore streams + redirectStreams( oldIn, oldOut, oldErr ); + + System.out.println( String.format( "Solution took %01d.%03d secs", duration / 1000, duration % 1000 )); + + try (BufferedReader chk = new BufferedReader( new InputStreamReader( new ByteArrayInputStream( bos.toByteArray() ), UTF_8 ))) { + String[] expectedLines = expected.split( "\\s*[|]\\s*" ); + + int lineCount = 0; + String line; + for ( ; (line = chk.readLine()) != null; lineCount++ ) { + assertEquals( expectedLines[ lineCount ], line ); + } + assertEquals( expectedLines.length, lineCount ); + } + } + finally { + // restore streams + redirectStreams( oldIn, oldOut, oldErr ); + } + } + + static void redirectStreams( final InputStream input, final PrintStream output, final PrintStream error ) { + System.setIn( input ); + System.setOut( output ); + System.setErr( error ); + } +}