├── .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 | }
--------------------------------------------------------------------------------