sqids 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: d51e2c5981c5b59b42e1cd3ddcf953ca6f1f5dfa21998b066064081888523a07
4
+ data.tar.gz: f3279de8aa60f40dbe20f338d18731f71018958784aaf5e95b65e22a57cd5664
5
+ SHA512:
6
+ metadata.gz: 510e16706f4310fdaab202bfef003d2cd7ce622e79b606d2ea948bf8b04a5f5c0d000e06102447782a87ff25ee0395b7e02e3b24b242af7fe6038b1682e34bde
7
+ data.tar.gz: 9232d0467cf326a4860d09fcb5e820893e426f6963e0296f821baef7e9873d1ccdd9bba98846f82d02f5ea55c9780c4bd8b136167c2bd8e858cb8b6653409947
@@ -0,0 +1,20 @@
1
+ name: Ruby
2
+ on:
3
+ push:
4
+ branches: [ main ]
5
+ pull_request:
6
+ branches: [ main ]
7
+ jobs:
8
+ test:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - uses: actions/checkout@v3
12
+ - name: Set up Ruby
13
+ uses: ruby/setup-ruby@v1
14
+ with:
15
+ ruby-version: '3.1'
16
+ bundler-cache: true
17
+ - name: Install dependencies
18
+ run: bundle install
19
+ - name: Run tests
20
+ run: bundle exec rspec
data/.rubocop.yml ADDED
@@ -0,0 +1,3 @@
1
+ AllCops:
2
+ NewCops: enable
3
+ TargetRubyVersion: 3.1
data/CHANGELOG.md ADDED
@@ -0,0 +1,4 @@
1
+ # CHANGELOG
2
+
3
+ **v0.1.0:**
4
+ - First implementation of [the spec](https://github.com/sqids/sqids-spec)
data/Gemfile ADDED
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ # Specify your gem's dependencies in sqids.gemspec
6
+ gemspec
7
+
8
+ group :development do
9
+ gem 'rake'
10
+ end
11
+
12
+ group :test do
13
+ gem 'rspec'
14
+ end
data/LICENSE ADDED
@@ -0,0 +1,21 @@
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.
data/README.md ADDED
@@ -0,0 +1,63 @@
1
+ # [Sqids Ruby](https://sqids.org/ruby)
2
+
3
+ [![Github Actions](https://img.shields.io/github/actions/workflow/status/sqids/sqids-ruby/tests.yml)](https://github.com/sqids/sqids-ruby/actions)
4
+
5
+ Sqids (pronounced "squids") is a small library that lets you generate YouTube-looking IDs from numbers. It's good for link shortening, fast & URL-safe ID generation and decoding back into numbers for quicker database lookups.
6
+
7
+ ## Getting started
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'sqids'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ ```bash
18
+ bundle
19
+ ```
20
+
21
+ Or install it via:
22
+
23
+ ```bash
24
+ gem install sqids
25
+ ```
26
+
27
+ ## Examples
28
+
29
+ Simple encode & decode:
30
+
31
+ ```ruby
32
+ sqids = Sqids.new
33
+ id = sqids.encode([1, 2, 3]) # '8QRLaD'
34
+ numbers = sqids.decode(id) # [1, 2, 3]
35
+ ```
36
+
37
+ Randomize IDs by providing a custom alphabet:
38
+
39
+ ```ruby
40
+ sqids = Sqids.new(alphabet: 'FxnXM1kBN6cuhsAvjW3Co7l2RePyY8DwaU04Tzt9fHQrqSVKdpimLGIJOgb5ZE')
41
+ id = sqids.encode([1, 2, 3]) # 'B5aMa3'
42
+ numbers = sqids.decode(id) # [1, 2, 3]
43
+ ```
44
+
45
+ Enforce a *minimum* length for IDs:
46
+
47
+ ```ruby
48
+ sqids = Sqids.new(min_length: 10)
49
+ id = sqids.encode([1, 2, 3]) # '75JT1cd0dL'
50
+ numbers = sqids.decode(id) # [1, 2, 3]
51
+ ```
52
+
53
+ Prevent specific words from appearing anywhere in the auto-generated IDs:
54
+
55
+ ```ruby
56
+ sqids = Sqids.new(blocklist: Set.new(%w[word1 word2]))
57
+ id = sqids.encode([1, 2, 3]) # '8QRLaD'
58
+ numbers = sqids.decode(id) # [1, 2, 3]
59
+ ```
60
+
61
+ ## License
62
+
63
+ [MIT](LICENSE)
data/lib/sqids.rb ADDED
@@ -0,0 +1,209 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'set'
4
+
5
+ class Sqids
6
+ DEFAULT_ALPHABET = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
7
+ DEFAULT_MIN_LENGTH = 0
8
+ # rubocop:disable Metrics/CollectionLiteralLength, Layout/LineLength
9
+ DEFAULT_BLOCKLIST = %w[0rgasm 1d10t 1d1ot 1di0t 1diot 1eccacu10 1eccacu1o 1eccacul0
10
+ 1eccaculo 1mbec11e 1mbec1le 1mbeci1e 1mbecile a11upat0 a11upato a1lupat0 a1lupato aand ah01e ah0le aho1e ahole al1upat0 al1upato allupat0 allupato ana1 ana1e anal anale anus arrapat0 arrapato arsch arse ass b00b b00be b01ata b0ceta b0iata b0ob b0obe b0sta b1tch b1te b1tte ba1atkar balatkar bastard0 bastardo batt0na battona bitch bite bitte bo0b bo0be bo1ata boceta boiata boob boobe bosta bran1age bran1er bran1ette bran1eur bran1euse branlage branler branlette branleur branleuse c0ck c0g110ne c0g11one c0g1i0ne c0g1ione c0gl10ne c0gl1one c0gli0ne c0glione c0na c0nnard c0nnasse c0nne c0u111es c0u11les c0u1l1es c0u1lles c0ui11es c0ui1les c0uil1es c0uilles c11t c11t0 c11to c1it c1it0 c1ito cabr0n cabra0 cabrao cabron caca cacca cacete cagante cagar cagare cagna cara1h0 cara1ho caracu10 caracu1o caracul0 caraculo caralh0 caralho cazz0 cazz1mma cazzata cazzimma cazzo ch00t1a ch00t1ya ch00tia ch00tiya ch0d ch0ot1a ch0ot1ya ch0otia ch0otiya ch1asse ch1avata ch1er ch1ng0 ch1ngadaz0s ch1ngadazos ch1ngader1ta ch1ngaderita ch1ngar ch1ngo ch1ngues ch1nk chatte chiasse chiavata chier ching0 chingadaz0s chingadazos chingader1ta chingaderita chingar chingo chingues chink cho0t1a cho0t1ya cho0tia cho0tiya chod choot1a choot1ya chootia chootiya cl1t cl1t0 cl1to clit clit0 clito cock cog110ne cog11one cog1i0ne cog1ione cogl10ne cogl1one cogli0ne coglione cona connard connasse conne cou111es cou11les cou1l1es cou1lles coui11es coui1les couil1es couilles cracker crap cu10 cu1att0ne cu1attone cu1er0 cu1ero cu1o cul0 culatt0ne culattone culer0 culero culo cum cunt d11d0 d11do d1ck d1ld0 d1ldo damn de1ch deich depp di1d0 di1do dick dild0 dildo dyke encu1e encule enema enf01re enf0ire enfo1re enfoire estup1d0 estup1do estupid0 estupido etr0n etron f0da f0der f0ttere f0tters1 f0ttersi f0tze f0utre f1ca f1cker f1ga fag fica ficker figa foda foder fottere fotters1 fottersi fotze foutre fr0c10 fr0c1o fr0ci0 fr0cio fr0sc10 fr0sc1o fr0sci0 fr0scio froc10 froc1o froci0 frocio frosc10 frosc1o frosci0 froscio fuck g00 g0o g0u1ne g0uine gandu go0 goo gou1ne gouine gr0gnasse grognasse haram1 harami haramzade hund1n hundin id10t id1ot idi0t idiot imbec11e imbec1le imbeci1e imbecile j1zz jerk jizz k1ke kam1ne kamine kike leccacu10 leccacu1o leccacul0 leccaculo m1erda m1gn0tta m1gnotta m1nch1a m1nchia m1st mam0n mamahuev0 mamahuevo mamon masturbat10n masturbat1on masturbate masturbati0n masturbation merd0s0 merd0so merda merde merdos0 merdoso mierda mign0tta mignotta minch1a minchia mist musch1 muschi n1gger neger negr0 negre negro nerch1a nerchia nigger orgasm p00p p011a p01la p0l1a p0lla p0mp1n0 p0mp1no p0mpin0 p0mpino p0op p0rca p0rn p0rra p0uff1asse p0uffiasse p1p1 p1pi p1r1a p1rla p1sc10 p1sc1o p1sci0 p1scio p1sser pa11e pa1le pal1e palle pane1e1r0 pane1e1ro pane1eir0 pane1eiro panele1r0 panele1ro paneleir0 paneleiro patakha pec0r1na pec0rina pecor1na pecorina pen1s pendej0 pendejo penis pip1 pipi pir1a pirla pisc10 pisc1o pisci0 piscio pisser po0p po11a po1la pol1a polla pomp1n0 pomp1no pompin0 pompino poop porca porn porra pouff1asse pouffiasse pr1ck prick pussy put1za puta puta1n putain pute putiza puttana queca r0mp1ba11e r0mp1ba1le r0mp1bal1e r0mp1balle r0mpiba11e r0mpiba1le r0mpibal1e r0mpiballe rand1 randi rape recch10ne recch1one recchi0ne recchione retard romp1ba11e romp1ba1le romp1bal1e romp1balle rompiba11e rompiba1le rompibal1e rompiballe ruff1an0 ruff1ano ruffian0 ruffiano s1ut sa10pe sa1aud sa1ope sacanagem sal0pe salaud salope saugnapf sb0rr0ne sb0rra sb0rrone sbattere sbatters1 sbattersi sborr0ne sborra sborrone sc0pare sc0pata sch1ampe sche1se sche1sse scheise scheisse schlampe schwachs1nn1g schwachs1nnig schwachsinn1g schwachsinnig schwanz scopare scopata sexy sh1t shit slut sp0mp1nare sp0mpinare spomp1nare spompinare str0nz0 str0nza str0nzo stronz0 stronza stronzo stup1d stupid succh1am1 succh1ami succhiam1 succhiami sucker t0pa tapette test1c1e test1cle testic1e testicle tette topa tr01a tr0ia tr0mbare tr1ng1er tr1ngler tring1er tringler tro1a troia trombare turd twat vaffancu10 vaffancu1o vaffancul0 vaffanculo vag1na vagina verdammt verga w1chsen wank wichsen x0ch0ta x0chota xana xoch0ta xochota z0cc01a z0cc0la z0cco1a z0ccola z1z1 z1zi ziz1 zizi zocc01a zocc0la zocco1a zoccola].freeze
11
+ # rubocop:enable Metrics/CollectionLiteralLength, Layout/LineLength
12
+
13
+ def initialize(options = {})
14
+ alphabet = options[:alphabet] || DEFAULT_ALPHABET
15
+ min_length = options[:min_length] || DEFAULT_MIN_LENGTH
16
+ blocklist = options[:blocklist] || DEFAULT_BLOCKLIST
17
+
18
+ raise ArgumentError, 'Alphabet length must be at least 5' if alphabet.length < 5
19
+
20
+ if alphabet.chars.uniq.size != alphabet.length
21
+ raise ArgumentError,
22
+ 'Alphabet must contain unique characters'
23
+ end
24
+
25
+ unless min_length.is_a?(Integer) && min_length >= Sqids.min_value && min_length <= alphabet.length
26
+ raise TypeError,
27
+ "Minimum length has to be between #{Sqids.min_value} and #{alphabet.length}"
28
+ end
29
+
30
+ filtered_blocklist = blocklist.select do |word|
31
+ word.length >= 3 && (word.chars - alphabet.chars).empty?
32
+ end.to_set(&:downcase)
33
+
34
+ @alphabet = shuffle(alphabet)
35
+ @min_length = min_length
36
+ @blocklist = filtered_blocklist
37
+ end
38
+
39
+ def encode(numbers)
40
+ return '' if numbers.empty?
41
+
42
+ in_range_numbers = numbers.select { |n| n >= Sqids.min_value && n <= Sqids.max_value }
43
+ unless in_range_numbers.length == numbers.length
44
+ raise ArgumentError,
45
+ "Encoding supports numbers between #{Sqids.min_value} and #{Sqids.max_value}"
46
+ end
47
+
48
+ encode_numbers(in_range_numbers, partitioned: false)
49
+ end
50
+
51
+ def decode(id)
52
+ ret = []
53
+
54
+ return ret if id.empty?
55
+
56
+ alphabet_chars = @alphabet.chars
57
+ id.chars.each do |c|
58
+ return ret unless alphabet_chars.include?(c)
59
+ end
60
+
61
+ prefix = id[0]
62
+ offset = @alphabet.index(prefix)
63
+ alphabet = @alphabet.slice(offset, @alphabet.length) + @alphabet.slice(0, offset)
64
+ partition = alphabet[1]
65
+ alphabet = alphabet.slice(2, alphabet.length)
66
+
67
+ id = id[1, id.length]
68
+
69
+ partition_index = id.index(partition)
70
+ if !partition_index.nil? && partition_index.positive? && partition_index < id.length - 1
71
+ id = id[partition_index + 1, id.length]
72
+ alphabet = shuffle(alphabet)
73
+ end
74
+
75
+ while id.length.positive?
76
+ separator = alphabet[-1]
77
+ chunks = id.split(separator, 2)
78
+
79
+ if chunks.any?
80
+ alphabet_without_separator = alphabet.slice(0, alphabet.length - 1)
81
+ return [] unless chunks[0].chars.all? { |c| alphabet_without_separator.include?(c) }
82
+
83
+ ret.push(to_number(chunks[0], alphabet_without_separator))
84
+ alphabet = shuffle(alphabet) if chunks.length > 1
85
+ end
86
+
87
+ id = chunks.length > 1 ? chunks[1] : ''
88
+ end
89
+
90
+ ret
91
+ end
92
+
93
+ def self.min_value
94
+ 0
95
+ end
96
+
97
+ def self.max_value
98
+ defined?(Integer::MAX) ? Integer::MAX : ((2**((0.size * 8) - 2)) - 1)
99
+ end
100
+
101
+ private
102
+
103
+ def shuffle(alphabet)
104
+ chars = alphabet.chars
105
+
106
+ i = 0
107
+ j = chars.length - 1
108
+ while j.positive?
109
+ r = ((i * j) + chars[i].ord + chars[j].ord) % chars.length
110
+ chars[i], chars[r] = chars[r], chars[i]
111
+ i += 1
112
+ j -= 1
113
+ end
114
+
115
+ chars.join
116
+ end
117
+
118
+ def encode_numbers(numbers, partitioned: false)
119
+ offset = numbers.length
120
+ numbers.each_with_index do |v, i|
121
+ offset += @alphabet[v % @alphabet.length].ord + i
122
+ end
123
+ offset = offset % @alphabet.length
124
+
125
+ alphabet = @alphabet.slice(offset, @alphabet.length) + @alphabet.slice(0, offset)
126
+ prefix = alphabet[0]
127
+ partition = alphabet[1]
128
+ alphabet = alphabet.slice(2, alphabet.length)
129
+ ret = [prefix]
130
+
131
+ numbers.each_with_index do |num, i|
132
+ alphabet_without_separator = alphabet.slice(0, alphabet.length - 1)
133
+ ret.push(to_id(num, alphabet_without_separator))
134
+
135
+ next unless i < numbers.length - 1
136
+
137
+ separator = alphabet[-1]
138
+ if partitioned && i.zero?
139
+ ret.push(partition)
140
+ else
141
+ ret.push(separator)
142
+ end
143
+
144
+ alphabet = shuffle(alphabet)
145
+ end
146
+
147
+ id = ret.join
148
+
149
+ if @min_length > id.length
150
+ unless partitioned
151
+ numbers = [0] + numbers
152
+ id = encode_numbers(numbers, partitioned: true)
153
+ end
154
+
155
+ if @min_length > id.length
156
+ id = id[0] + alphabet[0,
157
+ @min_length - id.length] + id[1,
158
+ id.length - 1]
159
+ end
160
+ end
161
+
162
+ if blocked_id?(id)
163
+ if partitioned
164
+ numbers[0] += 1
165
+ else
166
+ numbers = [0] + numbers
167
+ end
168
+
169
+ id = encode_numbers(numbers, partitioned: true)
170
+ end
171
+
172
+ id
173
+ end
174
+
175
+ def to_id(num, alphabet)
176
+ id = []
177
+ chars = alphabet.chars
178
+
179
+ result = num
180
+ loop do
181
+ id.unshift(chars[result % chars.length])
182
+ result /= chars.length
183
+ break unless result.positive?
184
+ end
185
+
186
+ id.join
187
+ end
188
+
189
+ def to_number(id, alphabet)
190
+ chars = alphabet.chars
191
+ id.chars.reduce(0) { |a, v| (a * chars.length) + chars.index(v) }
192
+ end
193
+
194
+ def blocked_id?(id)
195
+ id = id.downcase
196
+
197
+ @blocklist.any? do |word|
198
+ if word.length <= id.length
199
+ if id.length <= 3 || word.length <= 3
200
+ id == word
201
+ elsif word.match?(/\d/)
202
+ id.start_with?(word) || id.end_with?(word)
203
+ else
204
+ id.include?(word)
205
+ end
206
+ end
207
+ end
208
+ end
209
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rspec'
4
+ require_relative '../lib/sqids'
5
+
6
+ describe Sqids do
7
+ it 'encodes and decodes using simple alphabet' do
8
+ sqids = Sqids.new(alphabet: '0123456789abcdef')
9
+
10
+ numbers = [1, 2, 3]
11
+ id = '4d9fd2'
12
+
13
+ expect(sqids.encode(numbers)).to eq(id)
14
+ expect(sqids.decode(id)).to eq(numbers)
15
+ end
16
+
17
+ it 'decodes after encoding with a short alphabet' do
18
+ sqids = Sqids.new(alphabet: 'abcde')
19
+
20
+ numbers = [1, 2, 3]
21
+ encoded = sqids.encode(numbers)
22
+
23
+ expect(sqids.decode(encoded)).to eq(numbers)
24
+ end
25
+
26
+ it 'decodes after encoding with a long alphabet' do
27
+ sqids = Sqids.new(alphabet:
28
+ 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_+|{}[];:\'"/?.>,<`~')
29
+
30
+ numbers = [1, 2, 3]
31
+ encoded = sqids.encode(numbers)
32
+
33
+ expect(sqids.decode(encoded)).to eq(numbers)
34
+ end
35
+
36
+ it 'fails when alphabet characters are repeated' do
37
+ expect do
38
+ Sqids.new(alphabet: 'aabcdefg')
39
+ end.to raise_error(ArgumentError)
40
+ end
41
+
42
+ it 'fails when alphabet is too short' do
43
+ expect do
44
+ Sqids.new(alphabet: 'abcd')
45
+ end.to raise_error(ArgumentError)
46
+ end
47
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rspec'
4
+ require 'set'
5
+ require_relative '../lib/sqids'
6
+
7
+ describe Sqids do
8
+ it 'uses default blocklist if no custom blocklist is provided' do
9
+ sqids = Sqids.new
10
+
11
+ expect(sqids.decode('sexy')).to eq([200_044])
12
+ expect(sqids.encode([200_044])).to eq('d171vI')
13
+ end
14
+
15
+ it 'does not use any blocklist if an empty blocklist is provided' do
16
+ sqids = Sqids.new(blocklist: Set.new([]))
17
+
18
+ expect(sqids.decode('sexy')).to eq([200_044])
19
+ expect(sqids.encode([200_044])).to eq('sexy')
20
+ end
21
+
22
+ it 'uses provided blocklist if non-empty blocklist is provided' do
23
+ sqids = Sqids.new(blocklist: Set.new(['AvTg']))
24
+
25
+ expect(sqids.decode('sexy')).to eq([200_044])
26
+ expect(sqids.encode([200_044])).to eq('sexy')
27
+
28
+ expect(sqids.decode('AvTg')).to eq([100_000])
29
+ expect(sqids.encode([100_000])).to eq('7T1X8k')
30
+ expect(sqids.decode('7T1X8k')).to eq([100_000])
31
+ end
32
+
33
+ it 'uses blocklist to prevent certain encodings' do
34
+ sqids = Sqids.new(blocklist: Set.new(%w[8QRLaD 7T1cd0dL UeIe imhw LfUQ]))
35
+
36
+ expect(sqids.encode([1, 2, 3])).to eq('TM0x1Mxz')
37
+ expect(sqids.decode('TM0x1Mxz')).to eq([1, 2, 3])
38
+ end
39
+
40
+ it 'can decode blocklist words' do
41
+ sqids = Sqids.new(blocklist: Set.new(%w[8QRLaD 7T1cd0dL RA8UeIe7 WM3Limhw LfUQh4HN]))
42
+
43
+ expect(sqids.decode('8QRLaD')).to eq([1, 2, 3])
44
+ expect(sqids.decode('7T1cd0dL')).to eq([1, 2, 3])
45
+ expect(sqids.decode('RA8UeIe7')).to eq([1, 2, 3])
46
+ expect(sqids.decode('WM3Limhw')).to eq([1, 2, 3])
47
+ expect(sqids.decode('LfUQh4HN')).to eq([1, 2, 3])
48
+ end
49
+
50
+ it 'matches against a short blocklist word' do
51
+ sqids = Sqids.new(blocklist: Set.new(['pPQ']))
52
+
53
+ expect(sqids.decode(sqids.encode([1_000]))).to eq([1_000])
54
+ end
55
+ end
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rspec'
4
+ require_relative '../lib/sqids'
5
+
6
+ describe 'Sqids' do
7
+ it 'simple' do
8
+ sqids = Sqids.new
9
+
10
+ numbers = [1, 2, 3]
11
+ id = '8QRLaD'
12
+
13
+ expect(sqids.encode(numbers)).to eq(id)
14
+ expect(sqids.decode(id)).to eq(numbers)
15
+ end
16
+
17
+ it 'different inputs' do
18
+ sqids = Sqids.new
19
+
20
+ numbers = [0, 0, 0, 1, 2, 3, 100, 1_000, 100_000, 1_000_000, Sqids.max_value]
21
+ expect(sqids.decode(sqids.encode(numbers))).to eq(numbers)
22
+ end
23
+
24
+ it 'incremental numbers' do
25
+ sqids = Sqids.new
26
+
27
+ ids = {
28
+ 'bV' => [0],
29
+ 'U9' => [1],
30
+ 'g8' => [2],
31
+ 'Ez' => [3],
32
+ 'V8' => [4],
33
+ 'ul' => [5],
34
+ 'O3' => [6],
35
+ 'AF' => [7],
36
+ 'ph' => [8],
37
+ 'n8' => [9]
38
+ }
39
+
40
+ ids.each do |id, numbers|
41
+ expect(sqids.encode(numbers)).to eq(id)
42
+ expect(sqids.decode(id)).to eq(numbers)
43
+ end
44
+ end
45
+
46
+ it 'incremental numbers, same index 0' do
47
+ sqids = Sqids.new
48
+
49
+ ids = {
50
+ 'SrIu' => [0, 0],
51
+ 'nZqE' => [0, 1],
52
+ 'tJyf' => [0, 2],
53
+ 'e86S' => [0, 3],
54
+ 'rtC7' => [0, 4],
55
+ 'sQ8R' => [0, 5],
56
+ 'uz2n' => [0, 6],
57
+ '7Td9' => [0, 7],
58
+ '3nWE' => [0, 8],
59
+ 'mIxM' => [0, 9]
60
+ }
61
+
62
+ ids.each do |id, numbers|
63
+ expect(sqids.encode(numbers)).to eq(id)
64
+ expect(sqids.decode(id)).to eq(numbers)
65
+ end
66
+ end
67
+
68
+ it 'incremental numbers, same index 1' do
69
+ sqids = Sqids.new
70
+
71
+ ids = {
72
+ 'SrIu' => [0, 0],
73
+ 'nbqh' => [1, 0],
74
+ 't4yj' => [2, 0],
75
+ 'eQ6L' => [3, 0],
76
+ 'r4Cc' => [4, 0],
77
+ 'sL82' => [5, 0],
78
+ 'uo2f' => [6, 0],
79
+ '7Zdq' => [7, 0],
80
+ '36Wf' => [8, 0],
81
+ 'm4xT' => [9, 0]
82
+ }
83
+
84
+ ids.each do |id, numbers|
85
+ expect(sqids.encode(numbers)).to eq(id)
86
+ expect(sqids.decode(id)).to eq(numbers)
87
+ end
88
+ end
89
+
90
+ it 'multi input' do
91
+ sqids = Sqids.new
92
+
93
+ numbers = (0..99).to_a
94
+ output = sqids.decode(sqids.encode(numbers))
95
+ expect(numbers).to eq(output)
96
+ end
97
+
98
+ it 'encoding no numbers' do
99
+ sqids = Sqids.new
100
+ expect(sqids.encode([])).to eq('')
101
+ end
102
+
103
+ it 'decoding empty string' do
104
+ sqids = Sqids.new
105
+ expect(sqids.decode('')).to eq([])
106
+ end
107
+
108
+ it 'decoding an ID with an invalid character' do
109
+ sqids = Sqids.new
110
+ expect(sqids.decode('*')).to eq([])
111
+ end
112
+
113
+ it 'decoding an invalid ID with a repeating reserved character' do
114
+ sqids = Sqids.new
115
+ expect(sqids.decode('fff')).to eq([])
116
+ end
117
+
118
+ it 'encode out-of-range numbers' do
119
+ sqids = Sqids.new
120
+ expect { sqids.encode([Sqids.min_value - 1]) }.to raise_error(ArgumentError)
121
+ expect { sqids.encode([Sqids.max_value + 1]) }.to raise_error(ArgumentError)
122
+ end
123
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rspec'
4
+ require_relative '../lib/sqids'
5
+
6
+ describe Sqids do
7
+ let(:default_alphabet) { Sqids::DEFAULT_ALPHABET }
8
+
9
+ it 'encodes and decodes simple numbers' do
10
+ sqids = Sqids.new(min_length: default_alphabet.length)
11
+
12
+ numbers = [1, 2, 3]
13
+ id = '75JILToVsGerOADWmHlY38xvbaNZKQ9wdFS0B6kcMEtnRpgizhjU42qT1cd0dL'
14
+
15
+ expect(sqids.encode(numbers)).to eq(id)
16
+ expect(sqids.decode(id)).to eq(numbers)
17
+ end
18
+
19
+ it 'encodes and decodes incremental numbers' do
20
+ sqids = Sqids.new(min_length: default_alphabet.length)
21
+
22
+ ids = {
23
+ 'jf26PLNeO5WbJDUV7FmMtlGXps3CoqkHnZ8cYd19yIiTAQuvKSExzhrRghBlwf' => [0, 0],
24
+ 'vQLUq7zWXC6k9cNOtgJ2ZK8rbxuipBFAS10yTdYeRa3ojHwGnmMV4PDhESI2jL' => [0, 1],
25
+ 'YhcpVK3COXbifmnZoLuxWgBQwtjsSaDGAdr0ReTHM16yI9vU8JNzlFq5Eu2oPp' => [0, 2],
26
+ 'OTkn9daFgDZX6LbmfxI83RSKetJu0APihlsrYoz5pvQw7GyWHEUcN2jBqd4kJ9' => [0, 3],
27
+ 'h2cV5eLNYj1x4ToZpfM90UlgHBOKikQFvnW36AC8zrmuJ7XdRytIGPawqYEbBe' => [0, 4],
28
+ '7Mf0HeUNkpsZOTvmcj836P9EWKaACBubInFJtwXR2DSzgYGhQV5i4lLxoT1qdU' => [0, 5],
29
+ 'APVSD1ZIY4WGBK75xktMfTev8qsCJw6oyH2j3OnLcXRlhziUmpbuNEar05QCsI' => [0, 6],
30
+ 'P0LUhnlT76rsWSofOeyRGQZv1cC5qu3dtaJYNEXwk8Vpx92bKiHIz4MgmiDOF7' => [0, 7],
31
+ 'xAhypZMXYIGCL4uW0te6lsFHaPc3SiD1TBgw5O7bvodzjqUn89JQRfk2Nvm4JI' => [0, 8],
32
+ '94dRPIZ6irlXWvTbKywFuAhBoECQOVMjDJp53s2xeqaSzHY8nc17tmkLGwfGNl' => [0, 9]
33
+ }
34
+
35
+ ids.each do |id, numbers|
36
+ expect(sqids.encode(numbers)).to eq(id)
37
+ expect(sqids.decode(id)).to eq(numbers)
38
+ end
39
+ end
40
+
41
+ it 'encodes with different min lengths' do
42
+ [0, 1, 5, 10, default_alphabet.length].each do |min_length|
43
+ [
44
+ [Sqids.min_value],
45
+ [0, 0, 0, 0, 0],
46
+ [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
47
+ [100, 200, 300],
48
+ [1_000, 2_000, 3_000],
49
+ [1_000_000],
50
+ [Sqids.max_value]
51
+ ].each do |numbers|
52
+ sqids = Sqids.new(min_length:)
53
+
54
+ id = sqids.encode(numbers)
55
+ expect(id.length).to be >= min_length
56
+ expect(sqids.decode(id)).to eq(numbers)
57
+ end
58
+ end
59
+ end
60
+
61
+ it 'raises error for out-of-range invalid min lengths' do
62
+ expect { Sqids.new(min_length: -1) }.to raise_error(TypeError)
63
+ expect { Sqids.new(min_length: default_alphabet.length + 1) }.to raise_error(TypeError)
64
+ end
65
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rspec'
4
+ require_relative '../lib/sqids'
5
+
6
+ upper = 1_000_000
7
+
8
+ describe Sqids do
9
+ let(:sqids) { Sqids.new(min_length: Sqids::DEFAULT_ALPHABET.length) }
10
+
11
+ it 'uniques, with padding' do
12
+ set = Set.new
13
+
14
+ (0...upper).each do |i|
15
+ numbers = [i]
16
+ id = sqids.encode(numbers)
17
+ set.add(id)
18
+ expect(sqids.decode(id)).to eq(numbers)
19
+ end
20
+
21
+ expect(set.size).to eq(upper)
22
+ end
23
+
24
+ it 'uniques, low ranges' do
25
+ set = Set.new
26
+
27
+ (0...upper).each do |i|
28
+ numbers = [i]
29
+ id = sqids.encode(numbers)
30
+ set.add(id)
31
+ expect(sqids.decode(id)).to eq(numbers)
32
+ end
33
+
34
+ expect(set.size).to eq(upper)
35
+ end
36
+
37
+ it 'uniques, high ranges' do
38
+ set = Set.new
39
+
40
+ (100_000_000...(100_000_000 + upper)).each do |i|
41
+ numbers = [i]
42
+ id = sqids.encode(numbers)
43
+ set.add(id)
44
+ expect(sqids.decode(id)).to eq(numbers)
45
+ end
46
+
47
+ expect(set.size).to eq(upper)
48
+ end
49
+
50
+ it 'uniques, multi' do
51
+ set = Set.new
52
+
53
+ (0...upper).each do |i|
54
+ numbers = [i, i, i, i, i]
55
+ id = sqids.encode(numbers)
56
+ set.add(id)
57
+ expect(sqids.decode(id)).to eq(numbers)
58
+ end
59
+
60
+ expect(set.size).to eq(upper)
61
+ end
62
+ end
data/sqids.gemspec ADDED
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'English'
4
+ lib = File.expand_path('lib', __dir__)
5
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
6
+ require 'sqids'
7
+
8
+ Gem::Specification.new do |gem|
9
+ gem.name = 'sqids'
10
+ gem.version = '0.1.0'
11
+ gem.authors = ['Sqids Maintainers']
12
+ gem.summary = 'Generate YouTube-like ids from numbers.'
13
+ gem.homepage = 'https://sqids.org/ruby'
14
+ gem.license = 'MIT'
15
+
16
+ gem.required_ruby_version = '>= 3.1'
17
+
18
+ gem.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
19
+ gem.require_paths = ['lib']
20
+ gem.metadata['rubygems_mfa_required'] = 'true'
21
+ end
metadata ADDED
@@ -0,0 +1,56 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sqids
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Sqids Maintainers
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-08-12 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description:
14
+ email:
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - ".github/workflows/tests.yml"
20
+ - ".rubocop.yml"
21
+ - CHANGELOG.md
22
+ - Gemfile
23
+ - LICENSE
24
+ - README.md
25
+ - lib/sqids.rb
26
+ - spec/alphabet_spec.rb
27
+ - spec/blocklist_spec.rb
28
+ - spec/encoding_spec.rb
29
+ - spec/minlength_spec.rb
30
+ - spec/uniques_spec.rb
31
+ - sqids.gemspec
32
+ homepage: https://sqids.org/ruby
33
+ licenses:
34
+ - MIT
35
+ metadata:
36
+ rubygems_mfa_required: 'true'
37
+ post_install_message:
38
+ rdoc_options: []
39
+ require_paths:
40
+ - lib
41
+ required_ruby_version: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ version: '3.1'
46
+ required_rubygems_version: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: '0'
51
+ requirements: []
52
+ rubygems_version: 3.4.10
53
+ signing_key:
54
+ specification_version: 4
55
+ summary: Generate YouTube-like ids from numbers.
56
+ test_files: []