From dfd625a003eca3ba9d69e24e12db7c8e7ba2238c Mon Sep 17 00:00:00 2001 From: Teodor Grigor Date: Wed, 21 Feb 2024 03:50:59 +0200 Subject: [PATCH 1/2] Add @InternalSudoklifyApi annotation for internal library APIs --- sudoklify-common/api/sudoklify-common.api | 3 +++ .../sudoklify/common/InternalSudoklifyApi.kt | 22 +++++++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 sudoklify-common/src/main/kotlin/dev/teogor/sudoklify/common/InternalSudoklifyApi.kt diff --git a/sudoklify-common/api/sudoklify-common.api b/sudoklify-common/api/sudoklify-common.api index bb65a0f..5fdfb7e 100644 --- a/sudoklify-common/api/sudoklify-common.api +++ b/sudoklify-common/api/sudoklify-common.api @@ -1,3 +1,6 @@ +public abstract interface annotation class dev/teogor/sudoklify/common/InternalSudoklifyApi : java/lang/annotation/Annotation { +} + public final class dev/teogor/sudoklify/common/model/Sudoku { public fun ([[Ljava/lang/String;[[Ljava/lang/String;Ldev/teogor/sudoklify/common/types/Difficulty;Ldev/teogor/sudoklify/common/types/SudokuType;)V public final fun component1 ()[[Ljava/lang/String; diff --git a/sudoklify-common/src/main/kotlin/dev/teogor/sudoklify/common/InternalSudoklifyApi.kt b/sudoklify-common/src/main/kotlin/dev/teogor/sudoklify/common/InternalSudoklifyApi.kt new file mode 100644 index 0000000..8adf36e --- /dev/null +++ b/sudoklify-common/src/main/kotlin/dev/teogor/sudoklify/common/InternalSudoklifyApi.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2024 Teogor (Teodor Grigor) + * + * 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. + */ + +package dev.teogor.sudoklify.common + +@RequiresOptIn(message = "This API is internal to Sudoklify library. Do NOT use it!") +@Retention(AnnotationRetention.BINARY) +@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY) +annotation class InternalSudoklifyApi From 11eee13af9c02c055e095a6b94143f535f5bfb7a Mon Sep 17 00:00:00 2001 From: Teodor Grigor Date: Wed, 21 Feb 2024 03:51:17 +0200 Subject: [PATCH 2/2] Implement Sudoku board encoding and decoding functions --- .../common/types/{Token.kt => BoardCell.kt} | 2 +- .../teogor/sudoklify/common/types/TokenMap.kt | 2 +- sudoklify-core/api/sudoklify-core.api | 4 - .../core/generation/SudokuGenerator.kt | 4 +- .../sudoklify/core/io/BoardSerialization.kt | 113 +----------------- .../teogor/sudoklify/core/io/SudokuParser.kt | 3 + sudoklify-ktx/api/sudoklify-ktx.api | 12 ++ .../sudoklify/ktx/BoardCellExtensions.kt | 61 ++++++++++ .../sudoklify/ktx/SudokuBoardExtensions.kt | 61 ++++++++++ 9 files changed, 145 insertions(+), 117 deletions(-) rename sudoklify-common/src/main/kotlin/dev/teogor/sudoklify/common/types/{Token.kt => BoardCell.kt} (96%) create mode 100644 sudoklify-ktx/src/main/kotlin/dev/teogor/sudoklify/ktx/BoardCellExtensions.kt create mode 100644 sudoklify-ktx/src/main/kotlin/dev/teogor/sudoklify/ktx/SudokuBoardExtensions.kt diff --git a/sudoklify-common/src/main/kotlin/dev/teogor/sudoklify/common/types/Token.kt b/sudoklify-common/src/main/kotlin/dev/teogor/sudoklify/common/types/BoardCell.kt similarity index 96% rename from sudoklify-common/src/main/kotlin/dev/teogor/sudoklify/common/types/Token.kt rename to sudoklify-common/src/main/kotlin/dev/teogor/sudoklify/common/types/BoardCell.kt index 5fda2e5..7792eca 100644 --- a/sudoklify-common/src/main/kotlin/dev/teogor/sudoklify/common/types/Token.kt +++ b/sudoklify-common/src/main/kotlin/dev/teogor/sudoklify/common/types/BoardCell.kt @@ -19,4 +19,4 @@ package dev.teogor.sudoklify.common.types /** * Typealias for representing a single cell value in a game board. */ -typealias Token = String +typealias BoardCell = String diff --git a/sudoklify-common/src/main/kotlin/dev/teogor/sudoklify/common/types/TokenMap.kt b/sudoklify-common/src/main/kotlin/dev/teogor/sudoklify/common/types/TokenMap.kt index d80d5ed..f601976 100644 --- a/sudoklify-common/src/main/kotlin/dev/teogor/sudoklify/common/types/TokenMap.kt +++ b/sudoklify-common/src/main/kotlin/dev/teogor/sudoklify/common/types/TokenMap.kt @@ -16,4 +16,4 @@ package dev.teogor.sudoklify.common.types -typealias TokenMap = Map +typealias TokenMap = Map diff --git a/sudoklify-core/api/sudoklify-core.api b/sudoklify-core/api/sudoklify-core.api index afc7e8a..7491430 100644 --- a/sudoklify-core/api/sudoklify-core.api +++ b/sudoklify-core/api/sudoklify-core.api @@ -17,11 +17,7 @@ public final class dev/teogor/sudoklify/core/generation/SudokuGeneratorKt { } public final class dev/teogor/sudoklify/core/io/BoardSerializationKt { - public static final fun decodeAsBoard (Ljava/lang/String;Ldev/teogor/sudoklify/common/types/SudokuType;Lkotlin/jvm/functions/Function1;)Ljava/util/List; - public static final fun encodeAsString (Ljava/util/List;Lkotlin/jvm/functions/Function1;)Ljava/lang/String; public static final fun generateTokenMap (I)Ljava/util/Map; - public static final fun toNumber (Ljava/lang/String;)I - public static final fun toToken (I)Ljava/lang/String; } public final class dev/teogor/sudoklify/core/io/SudokuParser { diff --git a/sudoklify-core/src/main/kotlin/dev/teogor/sudoklify/core/generation/SudokuGenerator.kt b/sudoklify-core/src/main/kotlin/dev/teogor/sudoklify/core/generation/SudokuGenerator.kt index 0e1d404..ac54786 100644 --- a/sudoklify-core/src/main/kotlin/dev/teogor/sudoklify/core/generation/SudokuGenerator.kt +++ b/sudoklify-core/src/main/kotlin/dev/teogor/sudoklify/core/generation/SudokuGenerator.kt @@ -27,12 +27,12 @@ import dev.teogor.sudoklify.common.types.Seed import dev.teogor.sudoklify.common.types.SudokuString import dev.teogor.sudoklify.common.types.SudokuType import dev.teogor.sudoklify.common.types.TokenMap -import dev.teogor.sudoklify.core.io.toToken import dev.teogor.sudoklify.core.tokenizer.Tokenizer import dev.teogor.sudoklify.core.util.sortRandom import dev.teogor.sudoklify.core.util.toBoard import dev.teogor.sudoklify.core.util.toSequenceString import dev.teogor.sudoklify.ktx.createSeed +import dev.teogor.sudoklify.ktx.toBoardCell import kotlin.math.sqrt import kotlin.random.Random @@ -228,7 +228,7 @@ internal class SudokuGenerator internal constructor( val tokenList = gridList.withIndex().map { (index, _) -> val value = if (index < boxDigits) (index + 1) else (index - boxDigits + 1) - value.toToken() + value.toBoardCell() }.shuffled(random) val tokenMap = diff --git a/sudoklify-core/src/main/kotlin/dev/teogor/sudoklify/core/io/BoardSerialization.kt b/sudoklify-core/src/main/kotlin/dev/teogor/sudoklify/core/io/BoardSerialization.kt index f810ade..8ecaaf0 100644 --- a/sudoklify-core/src/main/kotlin/dev/teogor/sudoklify/core/io/BoardSerialization.kt +++ b/sudoklify-core/src/main/kotlin/dev/teogor/sudoklify/core/io/BoardSerialization.kt @@ -16,132 +16,27 @@ package dev.teogor.sudoklify.core.io -import dev.teogor.sudoklify.common.types.SudokuType -import dev.teogor.sudoklify.common.types.Token +import dev.teogor.sudoklify.common.InternalSudoklifyApi import dev.teogor.sudoklify.common.types.TokenMap - -/** - * Converts an integer value to a base-36 string representation for use in - * game board encoding. - * - * - Handles the special case of 0 being represented as "-". - * - Uses nested `when` expressions for efficient character construction. - * - * @receiver The integer value to be converted. - * @return The string representation of the value. - */ -fun Int.toToken(): Token = - when { - this == 0 -> "-" - - else -> { - var valueCopy = this - buildString { - while (valueCopy > 0) { - val char = - when (val digit = (valueCopy % 10)) { - 0 -> 'j' - else -> ('a' + digit - 1) - } - append(char) - valueCopy /= 10 - } - reverse() - this[0] = this[0].uppercaseChar() - } - } - } - -/** - * Converts a string representation back to an integer value. - * - * - Handles the special case of "-" being represented as 0. - * - Uses `map` and `fold` with nested `when` expressions for efficient conversion. - * - * @receiver The string representation to be converted. - * @return The integer value represented by the token. - */ -fun Token.toNumber(): Int = - when { - this == "-" -> 0 - - else -> - map { char -> - when { - char.isUpperCase() -> { - char - 'A' + 1 - } - - else -> { - char - 'a' + 1 - } - } - }.fold(0) { acc, digit -> acc * 10 + digit } - } - -/** - * Serializes a list of lists (representing a game board) as a base-36 string. - * - * - Uses the provided valueMapper function to convert individual cell values - * to integers before encoding. - * - * @receiver The grid of values to be serialized. - * @param valueMapper A function to map each cell value to its corresponding - * integer for encoding. - * @return The base-36 string representation of the board. - */ -inline fun List>.encodeAsString(crossinline valueMapper: T.() -> Int) = - flatMap { cells -> - cells.map { cell -> - valueMapper(cell).toToken() - } - }.joinToString("") - -/** - * Deserializes a base-36 string into a list of lists (representing a game board). - * - * - Uses the provided gameType to validate the board size. - * - Uses the provided valueMapper function to convert decoded integers back to - * cell values. - * - * @receiver The string representation of the grid. - * @param sudokuType The sudoku type, providing information about the expected board - * size. - * @param valueMapper A function to map each decoded integer to its corresponding - * cell value. - * @return The decoded board as a list of lists. - */ -inline fun String.decodeAsBoard( - sudokuType: SudokuType, - crossinline valueMapper: Int.() -> T, -): List> { - val regex = Regex("([A-I][a-z]+)|-|[A-I]") - val matches = regex.findAll(this) - val matchedTokens = ArrayList() - matches.forEach { matchedTokens.add(it.value) } - return matchedTokens - .chunked(sudokuType.cells) - .map { row -> row.map { valueMapper(it.toNumber()) } } -} +import dev.teogor.sudoklify.ktx.toBoardCell /** * Generates a mapping between token values and their corresponding * string representations. * - * TODO annotation for internal use - * * @param boxDigits The number of digits used to represent each box * in the Sudoku puzzle. * * @return A `TokenMap` containing the mapping between token values * and their string representations. */ +@InternalSudoklifyApi fun generateTokenMap(boxDigits: Int): TokenMap { val gridList = (1..boxDigits) val tokenList = gridList.withIndex().map { (index, _) -> val value = if (index < boxDigits) (index + 1) else (index - boxDigits + 1) - value.toToken() + value.toBoardCell() } val tokenMap = diff --git a/sudoklify-core/src/main/kotlin/dev/teogor/sudoklify/core/io/SudokuParser.kt b/sudoklify-core/src/main/kotlin/dev/teogor/sudoklify/core/io/SudokuParser.kt index 1e2b9c6..757e83d 100644 --- a/sudoklify-core/src/main/kotlin/dev/teogor/sudoklify/core/io/SudokuParser.kt +++ b/sudoklify-core/src/main/kotlin/dev/teogor/sudoklify/core/io/SudokuParser.kt @@ -14,8 +14,11 @@ * limitations under the License. */ +@file:OptIn(InternalSudoklifyApi::class) + package dev.teogor.sudoklify.core.io +import dev.teogor.sudoklify.common.InternalSudoklifyApi import dev.teogor.sudoklify.common.types.SudokuString import dev.teogor.sudoklify.common.types.SudokuType import dev.teogor.sudoklify.core.util.toBoard diff --git a/sudoklify-ktx/api/sudoklify-ktx.api b/sudoklify-ktx/api/sudoklify-ktx.api index 25d09a1..cb6b52d 100644 --- a/sudoklify-ktx/api/sudoklify-ktx.api +++ b/sudoklify-ktx/api/sudoklify-ktx.api @@ -1,3 +1,8 @@ +public final class dev/teogor/sudoklify/ktx/BoardCellExtensionsKt { + public static final fun toBoardCell (I)Ljava/lang/String; + public static final fun toInt (Ljava/lang/String;)I +} + public final class dev/teogor/sudoklify/ktx/DifficultyExtensionsKt { public static final fun toLabel (Ldev/teogor/sudoklify/common/types/Difficulty;[Ljava/lang/String;)Ljava/lang/String; public static final fun toStars (Ldev/teogor/sudoklify/common/types/Difficulty;)Ljava/lang/String; @@ -12,6 +17,13 @@ public final class dev/teogor/sudoklify/ktx/SeedExtensionsKt { public static final fun toSeed (J)Ldev/teogor/sudoklify/common/types/Seed; } +public final class dev/teogor/sudoklify/ktx/SudokuBoardExtensionsKt { + public static final fun getCells (Ljava/lang/String;)Ljava/util/ArrayList; + public static final fun mapIndexedToSudokuBoard (Ljava/lang/String;Ldev/teogor/sudoklify/common/types/SudokuType;Lkotlin/jvm/functions/Function3;)Ljava/util/List; + public static final fun mapToSudokuBoard (Ljava/lang/String;Ldev/teogor/sudoklify/common/types/SudokuType;Lkotlin/jvm/functions/Function1;)Ljava/util/List; + public static final fun mapToSudokuString (Ljava/util/List;Lkotlin/jvm/functions/Function1;)Ljava/lang/String; +} + public final class dev/teogor/sudoklify/ktx/SudokuTypeExtensionsKt { public static final fun areCellsInSameBox (Ldev/teogor/sudoklify/common/types/SudokuType;II)Z public static final fun areCellsInSameBox (Ldev/teogor/sudoklify/common/types/SudokuType;IIII)Z diff --git a/sudoklify-ktx/src/main/kotlin/dev/teogor/sudoklify/ktx/BoardCellExtensions.kt b/sudoklify-ktx/src/main/kotlin/dev/teogor/sudoklify/ktx/BoardCellExtensions.kt new file mode 100644 index 0000000..69815c4 --- /dev/null +++ b/sudoklify-ktx/src/main/kotlin/dev/teogor/sudoklify/ktx/BoardCellExtensions.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2024 Teogor (Teodor Grigor) + * + * 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. + */ + +package dev.teogor.sudoklify.ktx + +import dev.teogor.sudoklify.common.types.BoardCell + +fun Int.toBoardCell(): BoardCell { + return when { + this == 0 -> "-" + + else -> { + var valueCopy = this + buildString { + while (valueCopy > 0) { + val char = + when (val digit = (valueCopy % 10)) { + 0 -> 'j' + else -> ('a' + digit - 1) + } + append(char) + valueCopy /= 10 + } + reverse() + this[0] = this[0].uppercaseChar() + } + } + } +} + +fun BoardCell.toInt(): Int { + return when { + this == "-" -> 0 + + else -> + map { char -> + when { + char.isUpperCase() -> { + char - 'A' + 1 + } + + else -> { + char - 'a' + 1 + } + } + }.fold(0) { acc, digit -> acc * 10 + digit } + } +} diff --git a/sudoklify-ktx/src/main/kotlin/dev/teogor/sudoklify/ktx/SudokuBoardExtensions.kt b/sudoklify-ktx/src/main/kotlin/dev/teogor/sudoklify/ktx/SudokuBoardExtensions.kt new file mode 100644 index 0000000..29066bd --- /dev/null +++ b/sudoklify-ktx/src/main/kotlin/dev/teogor/sudoklify/ktx/SudokuBoardExtensions.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2024 Teogor (Teodor Grigor) + * + * 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. + */ + +package dev.teogor.sudoklify.ktx + +import dev.teogor.sudoklify.common.InternalSudoklifyApi +import dev.teogor.sudoklify.common.types.SudokuType + +inline fun List>.mapToSudokuString(crossinline valueMapper: T.() -> Int): String { + return flatMap { cells -> + cells.map { cell -> + valueMapper(cell).toBoardCell() + } + }.joinToString("") +} + +@OptIn(InternalSudoklifyApi::class) +inline fun String.mapToSudokuBoard( + sudokuType: SudokuType, + crossinline valueMapper: Int.() -> T, +): List> { + return getCells() + .chunked(sudokuType.cells) + .map { row -> row.map { valueMapper(it.toInt()) } } +} + +@OptIn(InternalSudoklifyApi::class) +inline fun String.mapIndexedToSudokuBoard( + sudokuType: SudokuType, + crossinline valueMapper: (value: Int, row: Int, column: Int) -> T, +): List> { + return getCells() + .chunked(sudokuType.cells) + .mapIndexed { row, rowElements -> + rowElements.mapIndexed { column, value -> + valueMapper(value.toInt(), row, column) + } + } +} + +@InternalSudoklifyApi +fun String.getCells(): ArrayList { + val regex = Regex("([A-I][a-z]+)|-|[A-I]") + val matches = regex.findAll(this) + val matchedTokens = ArrayList() + matches.forEach { matchedTokens.add(it.value) } + return matchedTokens +}