├── .github └── workflows │ └── test.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src ├── main └── kotlin │ └── org │ └── sqids │ └── Sqids.kt └── test └── kotlin └── org └── sqids ├── AlphabetTests.kt ├── BlockListTests.kt ├── EncodeTests.kt └── MinLengthTests.kt /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | test: 14 | runs-on: ubuntu-22.04 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: actions/setup-java@v3 18 | with: 19 | distribution: 'corretto' 20 | java-version: '17' 21 | - uses: gradle/gradle-build-action@v2 22 | - run: gradle wrapper 23 | - run: ./gradlew test -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | build/ 3 | !gradle/wrapper/gradle-wrapper.jar 4 | !**/src/main/**/build/ 5 | !**/src/test/**/build/ 6 | 7 | ### IntelliJ IDEA ### 8 | .idea/ 9 | .idea/modules.xml 10 | .idea/jarRepositories.xml 11 | .idea/compiler.xml 12 | .idea/libraries/ 13 | *.iws 14 | *.iml 15 | *.ipr 16 | out/ 17 | !**/src/main/**/out/ 18 | !**/src/test/**/out/ 19 | 20 | ### Eclipse ### 21 | .apt_generated 22 | .classpath 23 | .factorypath 24 | .project 25 | .settings 26 | .springBeans 27 | .sts4-cache 28 | bin/ 29 | !**/src/main/**/bin/ 30 | !**/src/test/**/bin/ 31 | 32 | ### NetBeans ### 33 | /nbproject/private/ 34 | /nbbuild/ 35 | /dist/ 36 | /nbdist/ 37 | /.nb-gradle/ 38 | 39 | ### VS Code ### 40 | .vscode/ 41 | 42 | ### Mac OS ### 43 | .DS_Store -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | @todo -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023-present Sqids maintainers. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Sqids Kotlin](https://sqids.org/kotlin) 2 | 3 | [Sqids](https://sqids.org/kotlin) (*pronounced "squids"*) is a small library that lets you **generate unique IDs from numbers**. It's good for link shortening, fast & URL-safe ID generation and decoding back into numbers for quicker database lookups. 4 | 5 | Features: 6 | 7 | - **Encode multiple numbers** - generate short IDs from one or several non-negative numbers 8 | - **Quick decoding** - easily decode IDs back into numbers 9 | - **Unique IDs** - generate unique IDs by shuffling the alphabet once 10 | - **ID padding** - provide minimum length to make IDs more uniform 11 | - **URL safe** - auto-generated IDs do not contain common profanity 12 | - **Randomized output** - Sequential input provides nonconsecutive IDs 13 | - **Many implementations** - Support for [40+ programming languages](https://sqids.org/) 14 | 15 | ## 🧰 Use-cases 16 | 17 | Good for: 18 | 19 | - Generating IDs for public URLs (eg: link shortening) 20 | - Generating IDs for internal systems (eg: event tracking) 21 | - Decoding for quicker database lookups (eg: by primary keys) 22 | 23 | Not good for: 24 | 25 | - Sensitive data (this is not an encryption library) 26 | - User IDs (can be decoded revealing user count) 27 | 28 | ## 🚀 Getting started 29 | 30 | Install Sqids via: 31 | 32 | Gradle (Groovy) 33 | ```groovy 34 | implementation 'org.sqids:sqids-kotlin:0.1.1' 35 | ``` 36 | 37 | or 38 | 39 | Gradle (Kotlin) 40 | ```kotlin 41 | implementation("org.sqids:sqids-kotlin:0.1.1") 42 | ``` 43 | 44 | or 45 | 46 | Maven 47 | ```xml 48 | 49 | org.sqids 50 | sqids-kotlin 51 | 0.1.1 52 | 53 | ``` 54 | 55 | Import into project 56 | 57 | ```kotlin 58 | import org.sqids.Sqids 59 | ``` 60 | 61 | ## 👩‍💻 Examples 62 | 63 | Simple encode & decode: 64 | 65 | ```kotlin 66 | val sqids = Sqids() 67 | val id = sqids.encode(listOf(1, 2, 3)) // "86Rf07" 68 | val numbers = sqids.decode(id) // [1, 2, 3] 69 | ``` 70 | 71 | > **Note** 72 | > 🚧 Because of the algorithm's design, **multiple IDs can decode back into the same sequence of numbers**. If it's important to your design that IDs are canonical, you have to manually re-encode decoded numbers and check that the generated ID matches. 73 | 74 | Enforce a *minimum* length for IDs: 75 | 76 | ```kotlin 77 | val sqids = Sqids(minLength = 10) 78 | val id = sqids.encode(listOf(1, 2, 3)) // "86Rf07xd4z" 79 | val numbers = sqids.decode(id) // [1, 2, 3] 80 | ``` 81 | 82 | Randomize IDs by providing a custom alphabet: 83 | 84 | ```kotlin 85 | val sqids = Sqids(alphabet = "FxnXM1kBN6cuhsAvjW3Co7l2RePyY8DwaU04Tzt9fHQrqSVKdpimLGIJOgb5ZE") 86 | val id = sqids.encode(listOf(1, 2, 3)) // "B4aajs" 87 | val numbers = sqids.decode(id) // [1, 2, 3] 88 | ``` 89 | 90 | Prevent specific words from appearing anywhere in the auto-generated IDs: 91 | 92 | ```kotlin 93 | val sqids = Sqids(blockList = setOf("86Rf07")) 94 | val id = sqids.encode(listOf(1, 2, 3)) // "se8ojk" 95 | val numbers = sqids.decode(id) // [1, 2, 3] 96 | ``` 97 | 98 | ## 📝 License 99 | 100 | [MIT](LICENSE) -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("org.jetbrains.kotlin.jvm") version "1.6.0" 3 | id("java-library") 4 | id("maven-publish") 5 | id("signing") 6 | } 7 | 8 | var rootArtifactId = "sqids-kotlin" 9 | var projectUrl = "https://sqids.org/kotlin" 10 | group = "org.sqids" 11 | version = "0.1.1-SNAPSHOT" 12 | 13 | repositories { 14 | mavenCentral() 15 | } 16 | 17 | dependencies { 18 | testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") 19 | } 20 | 21 | tasks.named("test", Test) { 22 | useJUnitPlatform() 23 | 24 | maxHeapSize = "1G" 25 | 26 | testLogging { 27 | showStandardStreams = true 28 | } 29 | } 30 | 31 | publishing { 32 | publications { 33 | 34 | mavenJava(MavenPublication) { 35 | groupId = group 36 | artifactId = rootArtifactId 37 | version = version 38 | from components.java 39 | pom { 40 | name = "Sqids" 41 | description = "Generate short YouTube-looking IDs from numbers." 42 | url = projectUrl 43 | properties = [ 44 | "parent.groupId": "org.sonatype.oss", 45 | "parent.artifactId": "oss-parent", 46 | "parent.version": "7" 47 | ] 48 | licenses { 49 | license { 50 | name = "MIT License" 51 | url = "https://github.com/sqids/sqids-kotlin/blob/main/LICENSE" 52 | } 53 | } 54 | developers { 55 | developer { 56 | id = "kevinxmorales" 57 | name = "Kevin Morales" 58 | email = "kevinm2052@gmail.com" 59 | } 60 | } 61 | scm { 62 | connection = "scm:git:https://github.com/sqids/sqids-kotlin.git" 63 | developerConnection = "scm:git:ssh://git@github.com:sqids/sqids-kotlin.git" 64 | url = projectUrl 65 | } 66 | } 67 | } 68 | } 69 | repositories { 70 | maven { 71 | url = version.endsWith('SNAPSHOT') ? 72 | "https://s01.oss.sonatype.org/content/repositories/snapshots/" : 73 | "https://s01.oss.sonatype.org/content/repositories/releases/" 74 | credentials { 75 | username "${System.getenv("SONATYPE_USERNAME")}" 76 | password "${System.getenv("SONATYPE_PASSWORD")}" 77 | } 78 | } 79 | } 80 | signing { 81 | useGpgCmd() 82 | sign publishing.publications.mavenJava 83 | } 84 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqids/sqids-kotlin/d081dc61ad5038a213951c66088fad6aa4980b83/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Stop when "xargs" is not available. 209 | if ! command -v xargs >/dev/null 2>&1 210 | then 211 | die "xargs is not available" 212 | fi 213 | 214 | # Use "xargs" to parse quoted args. 215 | # 216 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 217 | # 218 | # In Bash we could simply go: 219 | # 220 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 221 | # set -- "${ARGS[@]}" "$@" 222 | # 223 | # but POSIX shell has neither arrays nor command substitution, so instead we 224 | # post-process each arg (as a line of input to sed) to backslash-escape any 225 | # character that might be a shell metacharacter, then use eval to reverse 226 | # that process (while maintaining the separation between arguments), and wrap 227 | # the whole thing up as a single "set" statement. 228 | # 229 | # This will of course break if any of these variables contains a newline or 230 | # an unmatched quote. 231 | # 232 | 233 | eval "set -- $( 234 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 235 | xargs -n1 | 236 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 237 | tr '\n' ' ' 238 | )" '"$@"' 239 | 240 | exec "$JAVACMD" "$@" 241 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if %ERRORLEVEL% equ 0 goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if %ERRORLEVEL% equ 0 goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | set EXIT_CODE=%ERRORLEVEL% 84 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 85 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 86 | exit /b %EXIT_CODE% 87 | 88 | :mainEnd 89 | if "%OS%"=="Windows_NT" endlocal 90 | 91 | :omega 92 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | 2 | rootProject.name = "sqids-kotlin" 3 | 4 | -------------------------------------------------------------------------------- /src/main/kotlin/org/sqids/Sqids.kt: -------------------------------------------------------------------------------- 1 | package org.sqids 2 | 3 | const val MINIMUM_LENGTH = 3 4 | const val MIN_LENGTH_LIMIT = 255 5 | const val DEFAULT_ALPHABET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" 6 | const val DEFAULT_MIN_LENGTH = 0 7 | val DEFAULT_BLOCK_LIST = setOf( 8 | "0rgasm", 9 | "1d10t", 10 | "1d1ot", 11 | "1di0t", 12 | "1diot", 13 | "1eccacu10", 14 | "1eccacu1o", 15 | "1eccacul0", 16 | "1eccaculo", 17 | "1mbec11e", 18 | "1mbec1le", 19 | "1mbeci1e", 20 | "1mbecile", 21 | "a11upat0", 22 | "a11upato", 23 | "a1lupat0", 24 | "a1lupato", 25 | "aand", 26 | "ah01e", 27 | "ah0le", 28 | "aho1e", 29 | "ahole", 30 | "al1upat0", 31 | "al1upato", 32 | "allupat0", 33 | "allupato", 34 | "ana1", 35 | "ana1e", 36 | "anal", 37 | "anale", 38 | "anus", 39 | "arrapat0", 40 | "arrapato", 41 | "arsch", 42 | "arse", 43 | "ass", 44 | "b00b", 45 | "b00be", 46 | "b01ata", 47 | "b0ceta", 48 | "b0iata", 49 | "b0ob", 50 | "b0obe", 51 | "b0sta", 52 | "b1tch", 53 | "b1te", 54 | "b1tte", 55 | "ba1atkar", 56 | "balatkar", 57 | "bastard0", 58 | "bastardo", 59 | "batt0na", 60 | "battona", 61 | "bitch", 62 | "bite", 63 | "bitte", 64 | "bo0b", 65 | "bo0be", 66 | "bo1ata", 67 | "boceta", 68 | "boiata", 69 | "boob", 70 | "boobe", 71 | "bosta", 72 | "bran1age", 73 | "bran1er", 74 | "bran1ette", 75 | "bran1eur", 76 | "bran1euse", 77 | "branlage", 78 | "branler", 79 | "branlette", 80 | "branleur", 81 | "branleuse", 82 | "c0ck", 83 | "c0g110ne", 84 | "c0g11one", 85 | "c0g1i0ne", 86 | "c0g1ione", 87 | "c0gl10ne", 88 | "c0gl1one", 89 | "c0gli0ne", 90 | "c0glione", 91 | "c0na", 92 | "c0nnard", 93 | "c0nnasse", 94 | "c0nne", 95 | "c0u111es", 96 | "c0u11les", 97 | "c0u1l1es", 98 | "c0u1lles", 99 | "c0ui11es", 100 | "c0ui1les", 101 | "c0uil1es", 102 | "c0uilles", 103 | "c11t", 104 | "c11t0", 105 | "c11to", 106 | "c1it", 107 | "c1it0", 108 | "c1ito", 109 | "cabr0n", 110 | "cabra0", 111 | "cabrao", 112 | "cabron", 113 | "caca", 114 | "cacca", 115 | "cacete", 116 | "cagante", 117 | "cagar", 118 | "cagare", 119 | "cagna", 120 | "cara1h0", 121 | "cara1ho", 122 | "caracu10", 123 | "caracu1o", 124 | "caracul0", 125 | "caraculo", 126 | "caralh0", 127 | "caralho", 128 | "cazz0", 129 | "cazz1mma", 130 | "cazzata", 131 | "cazzimma", 132 | "cazzo", 133 | "ch00t1a", 134 | "ch00t1ya", 135 | "ch00tia", 136 | "ch00tiya", 137 | "ch0d", 138 | "ch0ot1a", 139 | "ch0ot1ya", 140 | "ch0otia", 141 | "ch0otiya", 142 | "ch1asse", 143 | "ch1avata", 144 | "ch1er", 145 | "ch1ng0", 146 | "ch1ngadaz0s", 147 | "ch1ngadazos", 148 | "ch1ngader1ta", 149 | "ch1ngaderita", 150 | "ch1ngar", 151 | "ch1ngo", 152 | "ch1ngues", 153 | "ch1nk", 154 | "chatte", 155 | "chiasse", 156 | "chiavata", 157 | "chier", 158 | "ching0", 159 | "chingadaz0s", 160 | "chingadazos", 161 | "chingader1ta", 162 | "chingaderita", 163 | "chingar", 164 | "chingo", 165 | "chingues", 166 | "chink", 167 | "cho0t1a", 168 | "cho0t1ya", 169 | "cho0tia", 170 | "cho0tiya", 171 | "chod", 172 | "choot1a", 173 | "choot1ya", 174 | "chootia", 175 | "chootiya", 176 | "cl1t", 177 | "cl1t0", 178 | "cl1to", 179 | "clit", 180 | "clit0", 181 | "clito", 182 | "cock", 183 | "cog110ne", 184 | "cog11one", 185 | "cog1i0ne", 186 | "cog1ione", 187 | "cogl10ne", 188 | "cogl1one", 189 | "cogli0ne", 190 | "coglione", 191 | "cona", 192 | "connard", 193 | "connasse", 194 | "conne", 195 | "cou111es", 196 | "cou11les", 197 | "cou1l1es", 198 | "cou1lles", 199 | "coui11es", 200 | "coui1les", 201 | "couil1es", 202 | "couilles", 203 | "cracker", 204 | "crap", 205 | "cu10", 206 | "cu1att0ne", 207 | "cu1attone", 208 | "cu1er0", 209 | "cu1ero", 210 | "cu1o", 211 | "cul0", 212 | "culatt0ne", 213 | "culattone", 214 | "culer0", 215 | "culero", 216 | "culo", 217 | "cum", 218 | "cunt", 219 | "d11d0", 220 | "d11do", 221 | "d1ck", 222 | "d1ld0", 223 | "d1ldo", 224 | "damn", 225 | "de1ch", 226 | "deich", 227 | "depp", 228 | "di1d0", 229 | "di1do", 230 | "dick", 231 | "dild0", 232 | "dildo", 233 | "dyke", 234 | "encu1e", 235 | "encule", 236 | "enema", 237 | "enf01re", 238 | "enf0ire", 239 | "enfo1re", 240 | "enfoire", 241 | "estup1d0", 242 | "estup1do", 243 | "estupid0", 244 | "estupido", 245 | "etr0n", 246 | "etron", 247 | "f0da", 248 | "f0der", 249 | "f0ttere", 250 | "f0tters1", 251 | "f0ttersi", 252 | "f0tze", 253 | "f0utre", 254 | "f1ca", 255 | "f1cker", 256 | "f1ga", 257 | "fag", 258 | "fica", 259 | "ficker", 260 | "figa", 261 | "foda", 262 | "foder", 263 | "fottere", 264 | "fotters1", 265 | "fottersi", 266 | "fotze", 267 | "foutre", 268 | "fr0c10", 269 | "fr0c1o", 270 | "fr0ci0", 271 | "fr0cio", 272 | "fr0sc10", 273 | "fr0sc1o", 274 | "fr0sci0", 275 | "fr0scio", 276 | "froc10", 277 | "froc1o", 278 | "froci0", 279 | "frocio", 280 | "frosc10", 281 | "frosc1o", 282 | "frosci0", 283 | "froscio", 284 | "fuck", 285 | "g00", 286 | "g0o", 287 | "g0u1ne", 288 | "g0uine", 289 | "gandu", 290 | "go0", 291 | "goo", 292 | "gou1ne", 293 | "gouine", 294 | "gr0gnasse", 295 | "grognasse", 296 | "haram1", 297 | "harami", 298 | "haramzade", 299 | "hund1n", 300 | "hundin", 301 | "id10t", 302 | "id1ot", 303 | "idi0t", 304 | "idiot", 305 | "imbec11e", 306 | "imbec1le", 307 | "imbeci1e", 308 | "imbecile", 309 | "j1zz", 310 | "jerk", 311 | "jizz", 312 | "k1ke", 313 | "kam1ne", 314 | "kamine", 315 | "kike", 316 | "leccacu10", 317 | "leccacu1o", 318 | "leccacul0", 319 | "leccaculo", 320 | "m1erda", 321 | "m1gn0tta", 322 | "m1gnotta", 323 | "m1nch1a", 324 | "m1nchia", 325 | "m1st", 326 | "mam0n", 327 | "mamahuev0", 328 | "mamahuevo", 329 | "mamon", 330 | "masturbat10n", 331 | "masturbat1on", 332 | "masturbate", 333 | "masturbati0n", 334 | "masturbation", 335 | "merd0s0", 336 | "merd0so", 337 | "merda", 338 | "merde", 339 | "merdos0", 340 | "merdoso", 341 | "mierda", 342 | "mign0tta", 343 | "mignotta", 344 | "minch1a", 345 | "minchia", 346 | "mist", 347 | "musch1", 348 | "muschi", 349 | "n1gger", 350 | "neger", 351 | "negr0", 352 | "negre", 353 | "negro", 354 | "nerch1a", 355 | "nerchia", 356 | "nigger", 357 | "orgasm", 358 | "p00p", 359 | "p011a", 360 | "p01la", 361 | "p0l1a", 362 | "p0lla", 363 | "p0mp1n0", 364 | "p0mp1no", 365 | "p0mpin0", 366 | "p0mpino", 367 | "p0op", 368 | "p0rca", 369 | "p0rn", 370 | "p0rra", 371 | "p0uff1asse", 372 | "p0uffiasse", 373 | "p1p1", 374 | "p1pi", 375 | "p1r1a", 376 | "p1rla", 377 | "p1sc10", 378 | "p1sc1o", 379 | "p1sci0", 380 | "p1scio", 381 | "p1sser", 382 | "pa11e", 383 | "pa1le", 384 | "pal1e", 385 | "palle", 386 | "pane1e1r0", 387 | "pane1e1ro", 388 | "pane1eir0", 389 | "pane1eiro", 390 | "panele1r0", 391 | "panele1ro", 392 | "paneleir0", 393 | "paneleiro", 394 | "patakha", 395 | "pec0r1na", 396 | "pec0rina", 397 | "pecor1na", 398 | "pecorina", 399 | "pen1s", 400 | "pendej0", 401 | "pendejo", 402 | "penis", 403 | "pip1", 404 | "pipi", 405 | "pir1a", 406 | "pirla", 407 | "pisc10", 408 | "pisc1o", 409 | "pisci0", 410 | "piscio", 411 | "pisser", 412 | "po0p", 413 | "po11a", 414 | "po1la", 415 | "pol1a", 416 | "polla", 417 | "pomp1n0", 418 | "pomp1no", 419 | "pompin0", 420 | "pompino", 421 | "poop", 422 | "porca", 423 | "porn", 424 | "porra", 425 | "pouff1asse", 426 | "pouffiasse", 427 | "pr1ck", 428 | "prick", 429 | "pussy", 430 | "put1za", 431 | "puta", 432 | "puta1n", 433 | "putain", 434 | "pute", 435 | "putiza", 436 | "puttana", 437 | "queca", 438 | "r0mp1ba11e", 439 | "r0mp1ba1le", 440 | "r0mp1bal1e", 441 | "r0mp1balle", 442 | "r0mpiba11e", 443 | "r0mpiba1le", 444 | "r0mpibal1e", 445 | "r0mpiballe", 446 | "rand1", 447 | "randi", 448 | "rape", 449 | "recch10ne", 450 | "recch1one", 451 | "recchi0ne", 452 | "recchione", 453 | "retard", 454 | "romp1ba11e", 455 | "romp1ba1le", 456 | "romp1bal1e", 457 | "romp1balle", 458 | "rompiba11e", 459 | "rompiba1le", 460 | "rompibal1e", 461 | "rompiballe", 462 | "ruff1an0", 463 | "ruff1ano", 464 | "ruffian0", 465 | "ruffiano", 466 | "s1ut", 467 | "sa10pe", 468 | "sa1aud", 469 | "sa1ope", 470 | "sacanagem", 471 | "sal0pe", 472 | "salaud", 473 | "salope", 474 | "saugnapf", 475 | "sb0rr0ne", 476 | "sb0rra", 477 | "sb0rrone", 478 | "sbattere", 479 | "sbatters1", 480 | "sbattersi", 481 | "sborr0ne", 482 | "sborra", 483 | "sborrone", 484 | "sc0pare", 485 | "sc0pata", 486 | "sch1ampe", 487 | "sche1se", 488 | "sche1sse", 489 | "scheise", 490 | "scheisse", 491 | "schlampe", 492 | "schwachs1nn1g", 493 | "schwachs1nnig", 494 | "schwachsinn1g", 495 | "schwachsinnig", 496 | "schwanz", 497 | "scopare", 498 | "scopata", 499 | "sexy", 500 | "sh1t", 501 | "shit", 502 | "slut", 503 | "sp0mp1nare", 504 | "sp0mpinare", 505 | "spomp1nare", 506 | "spompinare", 507 | "str0nz0", 508 | "str0nza", 509 | "str0nzo", 510 | "stronz0", 511 | "stronza", 512 | "stronzo", 513 | "stup1d", 514 | "stupid", 515 | "succh1am1", 516 | "succh1ami", 517 | "succhiam1", 518 | "succhiami", 519 | "sucker", 520 | "t0pa", 521 | "tapette", 522 | "test1c1e", 523 | "test1cle", 524 | "testic1e", 525 | "testicle", 526 | "tette", 527 | "topa", 528 | "tr01a", 529 | "tr0ia", 530 | "tr0mbare", 531 | "tr1ng1er", 532 | "tr1ngler", 533 | "tring1er", 534 | "tringler", 535 | "tro1a", 536 | "troia", 537 | "trombare", 538 | "turd", 539 | "twat", 540 | "vaffancu10", 541 | "vaffancu1o", 542 | "vaffancul0", 543 | "vaffanculo", 544 | "vag1na", 545 | "vagina", 546 | "verdammt", 547 | "verga", 548 | "w1chsen", 549 | "wank", 550 | "wichsen", 551 | "x0ch0ta", 552 | "x0chota", 553 | "xana", 554 | "xoch0ta", 555 | "xochota", 556 | "z0cc01a", 557 | "z0cc0la", 558 | "z0cco1a", 559 | "z0ccola", 560 | "z1z1", 561 | "z1zi", 562 | "ziz1", 563 | "zizi", 564 | "zocc01a", 565 | "zocc0la", 566 | "zocco1a", 567 | "zoccola" 568 | ) 569 | 570 | class Sqids(private var alphabet: String = DEFAULT_ALPHABET, private val minLength: Int = DEFAULT_MIN_LENGTH, private var blockList: Set = DEFAULT_BLOCK_LIST){ 571 | init { 572 | if(alphabet.length != alphabet.toByteArray().size) { 573 | throw IllegalArgumentException("Alphabet cannot contain multibyte characters") 574 | } 575 | 576 | if(alphabet.length < 3) { 577 | throw IllegalArgumentException("Alphabet length must be at least $MINIMUM_LENGTH") 578 | } 579 | 580 | if(alphabet.toSet().size != alphabet.length) { 581 | throw IllegalArgumentException("Alphabet must contain unique characters") 582 | } 583 | 584 | if(minLength < 0 || minLength > MIN_LENGTH_LIMIT) { 585 | throw IllegalArgumentException("Minimum length has to be between 0 and $MIN_LENGTH_LIMIT") 586 | } 587 | 588 | val filteredBlockList = mutableSetOf() 589 | for(word in blockList) { 590 | if(word.length >= 3) { 591 | val wordLowercased = word.lowercase() 592 | val intersection = wordLowercased.filter { c -> alphabet.lowercase().contains(c) } 593 | if (intersection.length == wordLowercased.length) { 594 | filteredBlockList.add(wordLowercased) 595 | } 596 | } 597 | } 598 | 599 | alphabet = shuffle(alphabet) 600 | blockList = filteredBlockList 601 | } 602 | 603 | /** 604 | * Decodes an [id] back into a List of unsigned integers 605 | * 606 | * These are the cases where the return value might be an empty array: 607 | * - Empty [id] / empty string 608 | * - Non-alphabet character is found within [id] 609 | * 610 | * @return the decoded List of unsigned integers 611 | */ 612 | fun decode(id: String): List { 613 | val ret = mutableListOf() 614 | if (id.isEmpty()) { 615 | return ret 616 | } 617 | val alphabetSet = alphabet.toSet() 618 | for (c in id) { 619 | if (!alphabetSet.contains(c)) { 620 | return ret 621 | } 622 | } 623 | 624 | val prefix = id.first() 625 | val offset = alphabet.indexOf(prefix) 626 | var alphabet = StringBuilder(alphabet.substring(offset)) 627 | .append(alphabet, 0, offset) 628 | .reverse() 629 | .toString() 630 | var slicedId = id.substring(1) 631 | while (slicedId.isNotEmpty()) { 632 | val separator = alphabet.first() 633 | val chunks = slicedId.split(separator.toString().toRegex(), limit = 2) 634 | if (chunks.isNotEmpty()) { 635 | if (chunks.first().isEmpty()) { 636 | return ret 637 | } 638 | ret.add(toNumber(chunks.first(), alphabet.substring(1))) 639 | if (chunks.size > 1) { 640 | alphabet = shuffle(alphabet) 641 | } 642 | } 643 | slicedId = if (chunks.size > 1) chunks[1] else "" 644 | } 645 | return ret 646 | } 647 | 648 | /** 649 | * Encodes an array of [numbers] into an ID 650 | * 651 | * These are the cases where encoding might fail: 652 | * - One of the numbers passed is smaller than 0 or greater than `maxValue()` 653 | * - An n-number of attempts has been made to re-generated the ID, where n is alphabet length + 1 654 | * 655 | * @return the generated ID 656 | * @throws IllegalArgumentException if one of the numbers passed is smaller than 0 or greater than `maxValue()` 657 | * @throws IllegalStateException if n attempts have been made to re-generated the ID, where n is alphabet length + 1 658 | */ 659 | fun encode(numbers: List): String { 660 | if (numbers.isEmpty()) { 661 | return "" 662 | } 663 | 664 | require(numbers.all { it >= 0 }) { "Encoding supports numbers between 0 and ${Long.MAX_VALUE}" } 665 | return encodeNumbers(numbers) 666 | } 667 | 668 | /** 669 | * Internal method that encodes an array of [numbers] into an ID. 670 | * This method uses an [increment] to modify \the `offset` variable in order to re-generate the ID 671 | * 672 | * @return the generated ID 673 | */ 674 | private fun encodeNumbers(numbers: List, increment: Int = 0): String { 675 | 676 | check(increment <= alphabet.length) { "Reached max attempts to re-generate the ID" } 677 | 678 | var offset = numbers.size 679 | for (i in numbers.indices) { 680 | val index = (numbers[i] % alphabet.length).toInt() 681 | offset += alphabet[index].code + i 682 | } 683 | offset %= alphabet.length 684 | offset = (offset + increment) % alphabet.length 685 | 686 | var alphabet = StringBuilder(alphabet.substring(offset)) 687 | .append(alphabet, 0, offset) 688 | .reverse() 689 | .toString() 690 | val prefix = alphabet.last() 691 | var id = StringBuilder().append(prefix) 692 | for ((i, num) in numbers.withIndex()) { 693 | id.append(toId(num, alphabet.substring(1))) 694 | if (i < numbers.size - 1) { 695 | id.append(alphabet.first()) 696 | alphabet = shuffle(alphabet) 697 | } 698 | } 699 | if (minLength > id.length) { 700 | id.append(alphabet.first()) 701 | while (minLength - id.length > 0) { 702 | alphabet = shuffle(alphabet) 703 | id.append(alphabet, 0, (minLength - id.length).coerceAtMost(alphabet.length)) 704 | } 705 | } 706 | if (isBlockedId(id.toString())) { 707 | id = id.clear() 708 | id.append(encodeNumbers(numbers, increment + 1)) 709 | } 710 | return id.toString() 711 | } 712 | 713 | 714 | private fun shuffle(alphabet: String): String { 715 | val chars = alphabet.toCharArray() 716 | for (i in 0 until chars.size - 1) { 717 | val j = chars.size - 1 - i 718 | val r = (i * j + chars[i].code + chars[j].code) % chars.size 719 | val temp = chars[i] 720 | chars[i] = chars[r] 721 | chars[r] = temp 722 | } 723 | return chars.joinToString(""); 724 | } 725 | 726 | private fun toId(num: Long, alphabet: String): String { 727 | var result = num 728 | val id = StringBuilder() 729 | do { 730 | id.append(alphabet[(result % alphabet.length).toInt()]) 731 | result = Math.floorDiv(result, alphabet.length) 732 | } while (result > 0) 733 | 734 | return id.reverse().toString() 735 | } 736 | 737 | private fun toNumber(id: String, alphabet: String): Long = id.fold(0) { 738 | number, c -> number * alphabet.length + alphabet.indexOf(c) 739 | } 740 | 741 | private fun isBlockedId(id: String): Boolean { 742 | val lowercaseId = id.lowercase() 743 | for(word in blockList) { 744 | if(word.length <= lowercaseId.length) { 745 | if(lowercaseId.length <= 3 || word.length <= 3) { 746 | if(lowercaseId == word) { 747 | return true 748 | } 749 | } else if(word.contains("[0-9]".toRegex())) { 750 | if (lowercaseId.startsWith(word) || lowercaseId.endsWith(word)) { 751 | return true 752 | } 753 | } else if(lowercaseId.contains(word)) { 754 | return true 755 | } 756 | } 757 | } 758 | return false 759 | } 760 | } 761 | -------------------------------------------------------------------------------- /src/test/kotlin/org/sqids/AlphabetTests.kt: -------------------------------------------------------------------------------- 1 | package org.sqids 2 | 3 | import kotlin.test.assertEquals 4 | import kotlin.test.assertFailsWith 5 | import kotlin.test.Test 6 | 7 | class AlphabetTests { 8 | @Test 9 | fun simpleAlphabet() { 10 | val sqids = Sqids(alphabet = "0123456789abcdef") 11 | val numbers = listOf(1L, 2L, 3L) 12 | val id = "489158" 13 | assertEquals(sqids.encode(numbers), id) 14 | assertEquals(sqids.decode(id), numbers) 15 | } 16 | 17 | @Test 18 | fun shortAlphabet() { 19 | val sqids = Sqids(alphabet = "abc") 20 | val numbers = listOf(1L, 2L, 3L) 21 | assertEquals(sqids.decode(sqids.encode(numbers)), numbers) 22 | } 23 | 24 | @Test 25 | fun longAlphabet() { 26 | val sqids = Sqids(alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#\$%^&*()-_+|{}[];:\\'\"/?.>,<`~") 27 | val numbers = listOf(1L, 2L, 3L) 28 | assertEquals(sqids.decode(sqids.encode(numbers)), numbers) 29 | } 30 | 31 | @Test 32 | fun multibyteCharacters() { 33 | val exception = assertFailsWith { 34 | Sqids(alphabet = "ë1092") 35 | } 36 | assertEquals("Alphabet cannot contain multibyte characters", exception.message) 37 | } 38 | 39 | @Test 40 | fun repeatingAlphabetCharacters() { 41 | val exception = assertFailsWith { 42 | Sqids(alphabet = "aabcdefg") 43 | } 44 | assertEquals("Alphabet must contain unique characters", exception.message) 45 | } 46 | 47 | @Test 48 | fun tooShortOfAnAlphabet() { 49 | val exception = assertFailsWith { 50 | Sqids("ab") 51 | } 52 | assertEquals("Alphabet length must be at least 3", exception.message) 53 | } 54 | } -------------------------------------------------------------------------------- /src/test/kotlin/org/sqids/BlockListTests.kt: -------------------------------------------------------------------------------- 1 | package org.sqids 2 | 3 | import kotlin.test.assertEquals 4 | import kotlin.test.assertFailsWith 5 | import kotlin.test.Test 6 | 7 | class BlockListTests { 8 | @Test 9 | fun blockList() { 10 | val sqids = Sqids() 11 | val numbers = listOf(4572721L) 12 | assertEquals(sqids.decode("aho1e"), numbers) 13 | assertEquals(sqids.encode(numbers), "JExTR") 14 | } 15 | 16 | @Test 17 | fun emptyBlockList() { 18 | val sqids = Sqids(blockList = setOf()) 19 | val numbers = listOf(4572721L) 20 | assertEquals(sqids.decode("aho1e"), numbers) 21 | assertEquals(sqids.encode(numbers), "aho1e") 22 | } 23 | 24 | @Test 25 | fun nonEmptyBlockList() { 26 | val sqids = Sqids(blockList = setOf("ArUO")) 27 | var numbers = listOf(4572721L) 28 | 29 | assertEquals(sqids.decode("aho1e"), numbers) 30 | assertEquals(sqids.encode(numbers), "aho1e") 31 | 32 | numbers = listOf(100000L) 33 | assertEquals(sqids.decode("ArUO"), numbers) 34 | assertEquals(sqids.encode(numbers), "QyG4") 35 | assertEquals(sqids.decode("QyG4"), numbers) 36 | } 37 | 38 | @Test 39 | fun encodeBlockList() { 40 | val sqids = Sqids(blockList = setOf( 41 | "JSwXFaosAN", // normal result of 1st encoding, let's block that word on purpose 42 | "OCjV9JK64o", // result of 2nd encoding 43 | "rBHf", // result of 3rd encoding is `4rBHfOiqd3`, let's block a substring 44 | "79SM", // result of 4th encoding is `dyhgw479SM`, let's block the postfix 45 | "7tE6" // result of 4th encoding is `7tE6jdAHLe`, let's block the prefix 46 | )) 47 | val numbers = listOf(1_000_000L, 2_000_000L) 48 | assertEquals(sqids.encode(numbers), "1aYeB7bRUt") 49 | assertEquals(sqids.decode("1aYeB7bRUt"), numbers) 50 | } 51 | 52 | @Test 53 | fun decodeBlockList() { 54 | val sqids = Sqids(blockList = setOf( 55 | "86Rf07", 56 | "se8ojk", 57 | "ARsz1p", 58 | "Q8AI49", 59 | "5sQRZO" 60 | )) 61 | val numbers = listOf(1L, 2L, 3L) 62 | assertEquals(sqids.decode("86Rf07"), numbers) 63 | assertEquals(sqids.decode("se8ojk"), numbers) 64 | assertEquals(sqids.decode("ARsz1p"), numbers) 65 | assertEquals(sqids.decode("Q8AI49"), numbers) 66 | assertEquals(sqids.decode("5sQRZO"), numbers) 67 | } 68 | 69 | @Test 70 | fun shortBlockList() { 71 | val sqids = Sqids(blockList = setOf("pnd")) 72 | val numbers = listOf(1000L) 73 | assertEquals(sqids.decode(sqids.encode(numbers)), numbers) 74 | } 75 | 76 | @Test 77 | fun lowercaseBlockList() { 78 | val sqids = Sqids( 79 | alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ", 80 | blockList = setOf("sxnzkl") 81 | ) 82 | val numbers = listOf(1L, 2L, 3L) 83 | assertEquals(sqids.encode(numbers), "IBSHOZ") 84 | assertEquals(sqids.decode("IBSHOZ"), numbers) 85 | } 86 | 87 | @Test 88 | fun maxBlockList() { 89 | val sqids = Sqids( 90 | alphabet = "abc", 91 | minLength = 3, 92 | blockList = setOf("cab", "abc", "bca") 93 | ) 94 | val exception = assertFailsWith { sqids.encode(listOf(0L)) } 95 | assertEquals("Reached max attempts to re-generate the ID", exception.message) 96 | } 97 | } -------------------------------------------------------------------------------- /src/test/kotlin/org/sqids/EncodeTests.kt: -------------------------------------------------------------------------------- 1 | package org.sqids 2 | 3 | import kotlin.test.assertEquals 4 | import kotlin.test.Test 5 | import kotlin.test.assertFailsWith 6 | 7 | class EncodeTests { 8 | private val sqids = Sqids() 9 | 10 | @Test 11 | fun simple() { 12 | val numbers = listOf(1L, 2L, 3L) 13 | val id = "86Rf07" 14 | assertEquals(sqids.encode(numbers), id) 15 | assertEquals(sqids.decode(id), numbers) 16 | } 17 | 18 | @Test 19 | fun differentInputs() { 20 | val numbers = listOf( 21 | 0L, 22 | 0L, 23 | 0L, 24 | 1L, 25 | 2L, 26 | 3L, 27 | 100L, 28 | 1000L, 29 | 100000L, 30 | 1000000L, 31 | Long.MAX_VALUE 32 | ) 33 | assertEquals(sqids.decode(sqids.encode(numbers)), numbers) 34 | } 35 | 36 | @Test 37 | fun incrementalNumber() { 38 | val ids = mapOf( 39 | "bM" to listOf(0L), 40 | "Uk" to listOf(1L), 41 | "gb" to listOf(2L), 42 | "Ef" to listOf(3L), 43 | "Vq" to listOf(4L), 44 | "uw" to listOf(5L), 45 | "OI" to listOf(6L), 46 | "AX" to listOf(7L), 47 | "p6" to listOf(8L), 48 | "nJ" to listOf(9L) 49 | ) 50 | for (id in ids.keys) { 51 | val numbers = ids[id]!! 52 | assertEquals(sqids.encode(numbers), id) 53 | assertEquals(sqids.decode(id), numbers) 54 | } 55 | } 56 | 57 | @Test 58 | fun incrementalNumbers() { 59 | var ids = mapOf( 60 | "SvIz" to listOf(0L, 0L), 61 | "n3qa" to listOf(0L, 1L), 62 | "tryF" to listOf(0L, 2L), 63 | "eg6q" to listOf(0L, 3L), 64 | "rSCF" to listOf(0L, 4L), 65 | "sR8x" to listOf(0L, 5L), 66 | "uY2M" to listOf(0L, 6L), 67 | "74dI" to listOf(0L, 7L), 68 | "30WX" to listOf(0L, 8L), 69 | "moxr" to listOf(0L, 9L) 70 | ) 71 | 72 | for ((id, numbers) in ids.entries) { 73 | assertEquals(sqids.encode(numbers), id) 74 | assertEquals(sqids.decode(id), numbers) 75 | } 76 | 77 | ids = mapOf( 78 | "SvIz" to listOf(0L, 0L), 79 | "nWqP" to listOf(1L, 0L), 80 | "tSyw" to listOf(2L, 0L), 81 | "eX68" to listOf(3L, 0L), 82 | "rxCY" to listOf(4L, 0L), 83 | "sV8a" to listOf(5L, 0L), 84 | "uf2K" to listOf(6L, 0L), 85 | "7Cdk" to listOf(7L, 0L), 86 | "3aWP" to listOf(8L, 0L), 87 | "m2xn" to listOf(9L, 0L) 88 | ) 89 | 90 | for ((id, numbers) in ids.entries) { 91 | assertEquals(sqids.encode(numbers), id) 92 | assertEquals(sqids.decode(id), numbers) 93 | } 94 | } 95 | 96 | @Test 97 | fun multiInput() { 98 | val numbers = listOf( 99 | 0L, 1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L, 9L, 10L, 11L, 12L, 13L, 14L, 15L, 16L, 17L, 18L, 19L, 20L, 100 | 21L, 22L, 23L, 24L, 25L, 26L, 27L, 28L, 29L, 30L, 31L, 32L, 33L, 34L, 35L, 36L, 37L, 38L, 101 | 39L, 40L, 41L, 42L, 43L, 44L, 45L, 46L, 47L, 48L, 49L, 50L, 51L, 52L, 53L, 54L, 55L, 56L, 102 | 57L, 58L, 59L, 60L, 61L, 62L, 63L, 64L, 65L, 66L, 67L, 68L, 69L, 70L, 71L, 72L, 73L, 74L, 103 | 75L, 76L, 77L, 78L, 79L, 80L, 81L, 82L, 83L, 84L, 85L, 86L, 87L, 88L, 89L, 90L, 91L, 92L, 104 | 93L, 94L, 95L, 96L, 97L, 98L, 99L 105 | ) 106 | assertEquals(sqids.decode(sqids.encode(numbers)), numbers) 107 | } 108 | 109 | @Test 110 | fun encodeNoNumbers() { 111 | assertEquals(sqids.encode(listOf()), "") 112 | } 113 | 114 | @Test 115 | fun decodeEmptyString() { 116 | assertEquals(sqids.decode(""), listOf()) 117 | } 118 | 119 | @Test 120 | fun decodeInvalidCharacter() { 121 | assertEquals(sqids.decode("*"), listOf()) 122 | } 123 | 124 | @Test 125 | fun encodeOutOfRangeNumbers() { 126 | val expectedException = "Encoding supports numbers between 0 and ${Long.MAX_VALUE}" 127 | var exception = assertFailsWith { sqids.encode(listOf(-1L)) } 128 | assertEquals(expectedException ,exception.message) 129 | exception = assertFailsWith { sqids.encode(listOf(Long.MAX_VALUE + 1)) } 130 | assertEquals(expectedException ,exception.message) 131 | } 132 | } -------------------------------------------------------------------------------- /src/test/kotlin/org/sqids/MinLengthTests.kt: -------------------------------------------------------------------------------- 1 | package org.sqids 2 | 3 | import kotlin.test.Test 4 | import kotlin.test.assertEquals 5 | import kotlin.test.assertFailsWith 6 | import kotlin.test.assertTrue 7 | 8 | class MinLengthTests { 9 | 10 | @Test 11 | fun simple() { 12 | val sqids = Sqids(minLength = DEFAULT_ALPHABET.length) 13 | val numbers = listOf(1L, 2L, 3L) 14 | val id = "86Rf07xd4zBmiJXQG6otHEbew02c3PWsUOLZxADhCpKj7aVFv9I8RquYrNlSTM" 15 | assertEquals(sqids.encode(numbers), id) 16 | assertEquals(sqids.decode(id), numbers) 17 | } 18 | 19 | @Test 20 | fun incremental() { 21 | val numbers = listOf(1L, 2L, 3L) 22 | val ids = mapOf( 23 | 6 to "86Rf07", 24 | 7 to "86Rf07x", 25 | 8 to "86Rf07xd", 26 | 9 to "86Rf07xd4", 27 | 10 to "86Rf07xd4z", 28 | 11 to "86Rf07xd4zB", 29 | 12 to "86Rf07xd4zBm", 30 | 13 to "86Rf07xd4zBmi", 31 | DEFAULT_ALPHABET.length + 0 to "86Rf07xd4zBmiJXQG6otHEbew02c3PWsUOLZxADhCpKj7aVFv9I8RquYrNlSTM", 32 | DEFAULT_ALPHABET.length + 1 to "86Rf07xd4zBmiJXQG6otHEbew02c3PWsUOLZxADhCpKj7aVFv9I8RquYrNlSTMy", 33 | DEFAULT_ALPHABET.length + 2 to "86Rf07xd4zBmiJXQG6otHEbew02c3PWsUOLZxADhCpKj7aVFv9I8RquYrNlSTMyf", 34 | DEFAULT_ALPHABET.length + 3 to "86Rf07xd4zBmiJXQG6otHEbew02c3PWsUOLZxADhCpKj7aVFv9I8RquYrNlSTMyf1" 35 | ) 36 | for ((minLength, id) in ids.entries) { 37 | val sqids = Sqids(minLength = minLength) 38 | assertEquals(sqids.encode(numbers), id) 39 | assertEquals(sqids.decode(id), numbers) 40 | } 41 | } 42 | 43 | @Test 44 | fun incrementalNumbers() { 45 | val sqids = Sqids(minLength = DEFAULT_ALPHABET.length) 46 | val ids = mapOf( 47 | "SvIzsqYMyQwI3GWgJAe17URxX8V924Co0DaTZLtFjHriEn5bPhcSkfmvOslpBu" to listOf(0L, 0L), 48 | "n3qafPOLKdfHpuNw3M61r95svbeJGk7aAEgYn4WlSjXURmF8IDqZBy0CT2VxQc" to listOf(0L, 1L), 49 | "tryFJbWcFMiYPg8sASm51uIV93GXTnvRzyfLleh06CpodJD42B7OraKtkQNxUZ" to listOf(0L, 2L), 50 | "eg6ql0A3XmvPoCzMlB6DraNGcWSIy5VR8iYup2Qk4tjZFKe1hbwfgHdUTsnLqE" to listOf(0L, 3L), 51 | "rSCFlp0rB2inEljaRdxKt7FkIbODSf8wYgTsZM1HL9JzN35cyoqueUvVWCm4hX" to listOf(0L, 4L), 52 | "sR8xjC8WQkOwo74PnglH1YFdTI0eaf56RGVSitzbjuZ3shNUXBrqLxEJyAmKv2" to listOf(0L, 5L), 53 | "uY2MYFqCLpgx5XQcjdtZK286AwWV7IBGEfuS9yTmbJvkzoUPeYRHr4iDs3naN0" to listOf(0L, 6L), 54 | "74dID7X28VLQhBlnGmjZrec5wTA1fqpWtK4YkaoEIM9SRNiC3gUJH0OFvsPDdy" to listOf(0L, 7L), 55 | "30WXpesPhgKiEI5RHTY7xbB1GnytJvXOl2p0AcUjdF6waZDo9Qk8VLzMuWrqCS" to listOf(0L, 8L), 56 | "moxr3HqLAK0GsTND6jowfZz3SUx7cQ8aC54Pl1RbIvFXmEJuBMYVeW9yrdOtin" to listOf(0L, 9L) 57 | ) 58 | for ((id, numbers) in ids.entries) { 59 | assertEquals(sqids.encode(numbers), id) 60 | assertEquals(sqids.decode(id), numbers) 61 | } 62 | } 63 | 64 | @Test 65 | fun minLengths() { 66 | val minLengths = listOf(0, 1, 5, 10, DEFAULT_ALPHABET.length) 67 | val numbers = listOf( 68 | listOf(0L), 69 | listOf(0L, 0L, 0L, 0L, 0L), 70 | listOf(1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L, 9L, 10L), 71 | listOf(100L, 200L, 300L), 72 | listOf(1_000L, 2_000L, 3_000L), 73 | listOf(Long.MAX_VALUE), 74 | ) 75 | for (minLength in minLengths) { 76 | val sqids = Sqids(minLength = minLength) 77 | for (number in numbers) { 78 | val id = sqids.encode(number) 79 | assertTrue(id.length >= minLength) 80 | assertEquals(sqids.decode(id), number) 81 | } 82 | } 83 | } 84 | 85 | @Test 86 | fun encodeOutOfRangeNumbers() { 87 | val expectedException = "Minimum length has to be between 0 and $MIN_LENGTH_LIMIT" 88 | val minLengthLimit = 255 89 | var exception = assertFailsWith { Sqids(minLength = -1) } 90 | assertEquals(expectedException, exception.message) 91 | exception = assertFailsWith { Sqids(minLength = minLengthLimit + 1) } 92 | assertEquals(expectedException, exception.message) 93 | } 94 | } --------------------------------------------------------------------------------