lexicoid 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 6c5ba2e47db70f981a8b76ba0424726acab9b805b97daad96ae00aeac5201ad4
4
+ data.tar.gz: 7654c97ba2b93b91b4930768c4834b3e1127ff9ba793475cf3f683023f44da89
5
+ SHA512:
6
+ metadata.gz: 9278e656eb7ae2506868f70d01920a88d17cfbde48218739bf6280b77ee721db1ae55f2f19ca4c08ce5eaac7c38c1e78c7447733c54aeb47a96c8f0556c2c55c
7
+ data.tar.gz: a49464b8626babea74781bc8cbfd864c565cb2b8a3939d49d86d497d9e06e5adf280de5ea9e0d112cc367ac7435f95651a5d1e654ed6b71c8e5bf2eedcced239
data/CHANGELOG.md ADDED
@@ -0,0 +1,13 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [0.1.0](https://github.com/michaelherold/lexicoid-ruby/tree/0.1.0) - 2023-03-19
8
+
9
+ ### Added
10
+
11
+ - Converting timestamps in the form of `Integer`s, `Float`s, `Time`s, `DateTime`s, and anything responding to `#to_date`, including `ActiveSupport::TimeWithZone` via `Lexicoid.from`.
12
+ - Generating the lexicoid for the current moment with `Lexicoid.now`.
13
+ - Converting timestamps with the `Kernel#Lexicoid` method, with optional exception squelching via `Lexicoid(Time.now, exception: false)`.
data/CONTRIBUTING.md ADDED
@@ -0,0 +1,55 @@
1
+ # Contributing
2
+
3
+ In the spirit of [free software](http://www.fsf.org/licensing/essays/free-sw.html), we encourage **everyone** to help improve this project. Here are some ways _you_ can contribute:
4
+
5
+ * Use alpha, beta, and pre-release versions.
6
+ * Report bugs.
7
+ * Suggest new features.
8
+ * Write or edit documentation.
9
+ * Write specifications.
10
+ * Write code (**no patch is too small**: fix typos, add comments, clean up inconsistent whitespace).
11
+ * Refactor code.
12
+ * Fix [issues].
13
+ * Review patches.
14
+
15
+ [issues]: https://github.com/michaelherold/lexicoid-ruby/issues
16
+
17
+ ## Submitting an Issue
18
+
19
+ We use the [GitHub issue tracker][issues] to track bugs and features. Before submitting a bug report or feature request, check to make sure no one has already submitted it.
20
+
21
+ When submitting a bug report, please include a `<details>` block that includes a stack trace and any details that may be necessary to reproduce the bug, including your gem version, Ruby version, and operating system. This looks like the following:
22
+
23
+ ```markdown
24
+ <details>
25
+ <summary>A description of the details block</summary>
26
+
27
+ All of the content that you want in here, perhaps with code fences. Note that
28
+ if you only have a code fence in here, you _must_ separate it from the <summary>
29
+ tag and the closing </details> or it won't render correctly.
30
+
31
+ Notice the empty line here ↓
32
+
33
+ </details>
34
+ ```
35
+
36
+ Ideally, a bug report should include a pull request with failing specs.
37
+
38
+ ## Submitting a Pull Request
39
+
40
+ 1. Fork the repository.
41
+ 2. Create a topic branch.
42
+ 3. Add specs for your unimplemented feature or bug fix.
43
+ 4. Run `bundle exec rake test`. If your specs pass, return to step 3.
44
+ 5. Implement your feature or bug fix.
45
+ 6. Run `bundle exec rake`. If your specs or any of the linters fail, return to step 5.
46
+ 7. Open `coverage/index.html`. If your changes are not fully covered by your tests, return to step 3.
47
+ 8. Add documentation for your feature or bug fix.
48
+ 9. Commit and push your changes.
49
+ 10. Submit a pull request.
50
+
51
+ ## Tools to Help You Succeed
52
+
53
+ After checking out the repository, run `bin/setup` to install dependencies. Then, run `bundle exec rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
54
+
55
+ Before committing code, run `bundle exec rake` to check that the code conforms to the style guidelines of the project, that all of the tests are green (if you're writing a feature; if you're only submitting a failing test, then it does not have to pass!), and that you sufficiently documented the changes.
data/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ # The MIT License (MIT)
2
+
3
+ Copyright © 2023 Michael Herold
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
13
+ all 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
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,92 @@
1
+ # Lexicoid for Ruby
2
+
3
+ Have you ever needed to convert a timestamp into something short, memorable, easy to look at, and easy to type? Look no further! You want a "lexicoid."
4
+
5
+ Inspired by [Brandur Leach](https://brandur.org/fragments/base32-slugs), Lexicoid for Ruby gives you lexicographically sortable, time-based identifiers that are easy to read and easy to type; perfect for using in URLs and other places that take human input.
6
+
7
+ Use lexicoids for anywhere that you have a timestamp that operates on "human scale" … think microblog status identifiers or URL-encoded timestamps. Avoid them for distributed systems; instead, consider something like [KSUID](https://github.com/michaelherold/ksuid-ruby) for that use case.
8
+
9
+ ## Installation
10
+
11
+ Install the gem and add it to the application's Gemfile by executing:
12
+
13
+ $ bundle add lexicoid
14
+
15
+ If you are not using Bundler to manage dependencies, install the gem by executing:
16
+
17
+ $ gem install lexicoid
18
+
19
+ ## Usage
20
+
21
+ Generate a lexical identifier for the current moment:
22
+
23
+ ```ruby
24
+ Lexicoid.now
25
+ ```
26
+
27
+ Convert any time-related object to a lexical identifier:
28
+
29
+ ```ruby
30
+ # Integer
31
+ Lexicoid.from(1_678_767_177)
32
+
33
+ # Float
34
+ Lexicoid.from(1_678_767_285.293477)
35
+
36
+ # Time
37
+ Lexicoid.from(Time.now)
38
+
39
+ # DateTime
40
+ Lexicoid.from(DateTime.now)
41
+
42
+ # ActiveSupport::TimeWithZone
43
+ Lexicoid.from(2.weeks.ago)
44
+ ```
45
+
46
+ There is also a `Kernel` method available for converting objects to lexicoids:
47
+
48
+ ```ruby
49
+ Lexicoid(Time.now)
50
+
51
+ Lexicoid("oops") #=> raises ArgumentError
52
+
53
+ Lexicoid("oops", exception: false) #=> ""
54
+ ```
55
+
56
+ ## Contributing
57
+
58
+ So you're interested in contributing to Lexicoid? Check out our [contributing guidelines](CONTRIBUTING.md) for more information on how to do that.
59
+
60
+ ## Supported Ruby Versions
61
+
62
+ This library aims to support and is [tested against](https://github.com/michaelherold/lexicoid-ruby/actions) the following Ruby versions:
63
+
64
+ * Ruby 3.0
65
+ * Ruby 3.1
66
+ * Ruby 3.2
67
+
68
+ If something doesn't work on one of these versions, it's a bug.
69
+
70
+ This library may inadvertently work (or seem to work) on other Ruby versions, however we will only provide support for the versions listed above.
71
+
72
+ If you would like this library to support another Ruby version or implementation, you may volunteer to be a maintainer. Being a maintainer entails making sure all tests run and pass on that implementation. When something breaks on your implementation, you will be responsible for providing patches in a timely fashion. If critical issues for a particular implementation exist at the time of a major release, we may drop support for that Ruby version.
73
+
74
+ ## Versioning
75
+
76
+ This library aims to adhere to [Semantic Versioning 2.0.0](http://semver.org/spec/v2.0.0.html). Report violations of this scheme should as bugs. Specifically, if a minor or patch version breaks backward compatibility, that version should be immediately yanked and/or a new version should be immediately released that restores compatibility. Only new major versions will introduce breaking changes to the public API. As a result of this policy, you can (and should) specify a dependency on this gem using the [pessimistic version constraint](http://guides.rubygems.org/patterns/#pessimistic-version-constraint) with two digits of precision. For example:
77
+
78
+ spec.add_dependency "lexicoid", "~> 0.1"
79
+
80
+ ## License
81
+
82
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
83
+
84
+ ## Code of Conduct
85
+
86
+ We expect everyone interacting in the Lexicoid for Ruby project's codebases, issue trackers, chat rooms and mailing lists to follow the [code of conduct](https://github.com/michaelherold/lexicoid/blob/main/CODE_OF_CONDUCT.md).
87
+
88
+ ## Acknowledgments
89
+
90
+ [Brandur Leach](https://brandur.org/fragments/base32-slugs) wrote about the concept behind this gem and inspired its creation.
91
+
92
+ [Luciano Mammino](https://loige.co/) took the first step, created a [Rust crate](https://crates.io/crates/lexicoid) and came up with the [excellent name](https://github.com/lmammino/lexicoid) for these identifiers.
data/lexicoid.gemspec ADDED
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/lexicoid/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "lexicoid"
7
+ spec.version = Lexicoid::VERSION
8
+ spec.authors = ["Michael Herold"]
9
+ spec.email = ["opensource@michaeljherold.com"]
10
+
11
+ spec.summary = "Short and stable IDs from timestamps"
12
+ spec.description = spec.summary
13
+ spec.homepage = "https://github.com/michaelherold/lexicoid-ruby"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = %w[CHANGELOG.md CONTRIBUTING.md LICENSE.md README.md]
17
+ spec.files += %w[lexicoid.gemspec]
18
+ spec.files += Dir["lib/**/*.rb"]
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.required_ruby_version = ">= 3.0.0"
22
+
23
+ spec.add_development_dependency "bundler", ">= 2"
24
+
25
+ spec.metadata = {
26
+ "bug_tracker_uri" => "https://github.com/michaelherold/lexicoid-ruby/issues",
27
+ "changelog_uri" => "https://github.com/michaelherold/lexicoid-ruby/blob/main/CHANGELOG.md",
28
+ "documentation_uri" => "https://rubydoc.info/gems/lexicoid-ruby/#{Lexicoid::VERSION}",
29
+ "homepage_uri" => "https://github.com/michaelherold/lexicoid-ruby",
30
+ "rubygems_mfa_required" => "true",
31
+ "source_code_uri" => "https://github.com/michaelherold/lexicoid-ruby"
32
+ }
33
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The `Kernel` module is included by class `Object`, so its methods are available in every Ruby object.
4
+ #
5
+ # @see https://ruby-doc.org/current/Kernel.html
6
+ module Kernel
7
+ private
8
+
9
+ # Returns a lexicographically sortable friendly ID for a timestamp
10
+ #
11
+ # @api public
12
+ # @since 0.1.0
13
+ # @!visibility public
14
+ #
15
+ # @example Converts a post's created-at timestamp to a friendly ID
16
+ # Post = Struct.new(:created_at, keyword_init: true)
17
+ # post = Post.new(created_at: Time.parse("2023-03-19 14:33:01.80251 -0500"))
18
+ #
19
+ # Lexicoid(post.created_at) #=> "gkfqavc"
20
+ #
21
+ # @param object [ActiveSupport::TimeWithZone, DateTime, Float, Integer, Time, #to_time]
22
+ # the object to convert into a lexicographically sorting friendly ID
23
+ # @param exception [Boolean] raises an ArgumentError when the argument is
24
+ # improper and this is true, otherwise an improper argument leads to an empty string
25
+ # @return [String] the friendly ID
26
+ # @raise [ArgumentError] when the object is not a timestamp-like one
27
+ def Lexicoid(object, exception: true)
28
+ Lexicoid.from(object)
29
+ rescue ArgumentError
30
+ raise if exception
31
+ ""
32
+ end
33
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lexicoid
4
+ # The version of the library
5
+ #
6
+ # @private
7
+ VERSION = "0.1.0"
8
+ end
data/lib/lexicoid.rb ADDED
@@ -0,0 +1,192 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lexicoid/version"
4
+
5
+ # Generates lexicographically sortable, friendly IDs from timestamps
6
+ #
7
+ # A "lexicoid" is a short identifier derived from a timestamp for use in single
8
+ # source streaming systems. They are based on an [approach for giving friendly
9
+ # URLs to microblog posts, by Brandur Leach][1].
10
+ #
11
+ # In order to make them easy to use, lexicoids have the following properties:
12
+ #
13
+ # 1. They are lexicographically, i.e. alphabetically, sortable by time to the
14
+ # second.
15
+ # 2. They encode to lower case letters for easier typing into a URL.
16
+ # 3. Their alphabet eliminates easily-confused letter combinations like `1`,
17
+ # `l`, and `I`, or `0` and `O`.
18
+ #
19
+ # At their heart, lexicoids are a special case of [Base 32 encoding][2] for
20
+ # integers.
21
+ #
22
+ # Their properties makes them ideally suitable to human-scale use cases, like
23
+ # microblog post slugs or URL-encoded timestamps. They are poorly suited for
24
+ # distributed systems with multiple clocks or systems that generate items at a
25
+ # rate of more than one per second, regardless of the number of clocks.
26
+ #
27
+ # [1]: https://brandur.org/fragments/base32-slugs
28
+ # [2]: https://datatracker.ietf.org/doc/html/rfc4648
29
+ module Lexicoid
30
+ # The Time instance representing the time at the beginning of the Unix epoch
31
+ #
32
+ # @api private
33
+ # @private
34
+ EPOCH = Time.at(0).utc.freeze
35
+
36
+ # The alphabet for encoding lexicographic base32 IDs
37
+ #
38
+ # @api private
39
+ # @private
40
+ LEXICOGRAPHIC_BASE32 = "234567abcdefghijklmnopqrstuvwxyz"
41
+
42
+ # Converts a timestamp into a lexicographically sortable, friendly ID
43
+ #
44
+ # @api public
45
+ # @since 0.1.0
46
+ #
47
+ # @example Generate a lexicoid from a DateTime
48
+ # Lexicoid.from DateTime.parse("2023-03-19T21:19:50-05:00")
49
+ #
50
+ # @example Generate a lexicoid from a Float timestamp
51
+ # Lexicoid.from 2_147_483_647.0
52
+ #
53
+ # @example Generate a lexicoid from an Integer timestamp
54
+ # Lexicoid.from 946_684_799
55
+ #
56
+ # @example Generate a lexicoid from a Time
57
+ # Lexicoid.from Time.parse("2023-03-19 15:06:14.48414 -0500")
58
+ #
59
+ # @example Generate a lexicoid in a Rails application
60
+ # Lexicoid.from 2.weeks.ago
61
+ #
62
+ # @param number_or_time [ActiveSupport::TimeWithZone, DateTime, Float, Integer, Time, #to_time]
63
+ # the object to convert into a lexicographically sorting friendly ID
64
+ # @return [String] the friendly ID
65
+ # @raise [ArgumentError] when the object is unsuitable for conversion
66
+ def self.from(number_or_time)
67
+ number = maybe_coerce_time(number_or_time)
68
+ bytes = bytes_from_number(number)
69
+ buffer = encode(bytes)
70
+
71
+ buffer.join
72
+ end
73
+
74
+ # Generates a lexicographically sortable, friendly ID from the current time
75
+ #
76
+ # @api public
77
+ # @since 0.1.0
78
+ #
79
+ # @example Generate a friendly ID for the current moment
80
+ # Lexicoid.now
81
+ #
82
+ # @return [String] the friendly ID
83
+ def self.now
84
+ from Time.now
85
+ end
86
+
87
+ # Extracts the bytes from a number in big endian order
88
+ #
89
+ # @api private
90
+ # @private
91
+ #
92
+ # @param number [Integer] the number to extract bytes from
93
+ # @return [Array<Integer>] the bytes from the number as integers in big endian
94
+ # order
95
+ private_class_method def self.bytes_from_number(number)
96
+ [].tap do |bytes|
97
+ bytes.unshift(0) and next if number.zero?
98
+
99
+ while number.positive?
100
+ bytes.unshift(number & 0xFF)
101
+ number >>= 8
102
+ end
103
+ end
104
+ end
105
+
106
+ # Encodes a byte array into a lexicographically sortable array of characters
107
+ #
108
+ # @api private
109
+ # @private
110
+ #
111
+ # @param bytes [Array<Integer>] the bytes to encode, in big endian order
112
+ # @return [Array<String>] the bytes encoded to the lexicographic base32 alphabet
113
+ private_class_method def self.encode(bytes)
114
+ result = Array.new((bytes.length * 8 + 4) / 5, 0)
115
+ offset = 0
116
+
117
+ while bytes.length.positive?
118
+ length = bytes.length
119
+ buffer =
120
+ case length
121
+ when 1, 2 then Array.new(bytes.length * 2, 0)
122
+ when 3, 4 then Array.new(bytes.length * 2 - 1, 0)
123
+ else Array.new(8, 0)
124
+ end
125
+
126
+ if length > 4
127
+ buffer[7] = bytes[4] & 0x1F
128
+ buffer[6] = bytes[4] >> 5
129
+ end
130
+
131
+ if length >= 4
132
+ buffer[6] |= (bytes[3] << 3) & 0x1F
133
+ buffer[5] = (bytes[3] >> 2) & 0x1F
134
+ buffer[4] = bytes[3] >> 7
135
+ end
136
+
137
+ if length >= 3
138
+ buffer[4] |= (bytes[2] << 1) & 0x1F
139
+ buffer[3] = (bytes[2] >> 4) & 0x1F
140
+ end
141
+
142
+ if length >= 2
143
+ buffer[3] |= (bytes[1] << 4) & 0x1F
144
+ buffer[2] = (bytes[1] >> 1) & 0x1F
145
+ buffer[1] = (bytes[1] >> 6) & 0x1F
146
+ end
147
+
148
+ if length >= 1
149
+ buffer[1] |= (bytes[0] << 2) & 0x1F
150
+ buffer[0] = bytes[0] >> 3
151
+ end
152
+
153
+ buffer.each_with_index do |byte, i|
154
+ result[i + offset] = LEXICOGRAPHIC_BASE32[byte & 31]
155
+ end
156
+
157
+ break if bytes.length < 5
158
+
159
+ bytes = bytes[5..]
160
+ offset += 8
161
+ end
162
+
163
+ result
164
+ end
165
+
166
+ # Validates and coerces an argument for suitability for conversion
167
+ #
168
+ # @api private
169
+ # @private
170
+ #
171
+ # @param object [Object] the object to validate and coerce
172
+ # @return [Integer] the object converted into an integer timestamp
173
+ # @raise [ArgumentError] when the object is unsuitable for conversion
174
+ private_class_method def self.maybe_coerce_time(object)
175
+ if object.is_a?(Integer) || object.is_a?(Float)
176
+ raise ArgumentError, "#{object} must be non-negative" if object.negative?
177
+ object.to_i
178
+ elsif object.is_a?(Time)
179
+ raise ArgumentError, "#{object.iso8601} must be after #{EPOCH.iso8601}" unless EPOCH <= object
180
+ object.to_i
181
+ elsif object.respond_to?(:to_time)
182
+ maybe_coerce_time object.to_time
183
+ else
184
+ raise(
185
+ ArgumentError,
186
+ "#{object.inspect} must be one of Integer, Float, Time, or respond to `#to_time'"
187
+ )
188
+ end
189
+ end
190
+ end
191
+
192
+ require_relative "core_ext/kernel"
metadata ADDED
@@ -0,0 +1,71 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lexicoid
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Michael Herold
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-03-20 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '2'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '2'
27
+ description: Short and stable IDs from timestamps
28
+ email:
29
+ - opensource@michaeljherold.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - CHANGELOG.md
35
+ - CONTRIBUTING.md
36
+ - LICENSE.md
37
+ - README.md
38
+ - lexicoid.gemspec
39
+ - lib/core_ext/kernel.rb
40
+ - lib/lexicoid.rb
41
+ - lib/lexicoid/version.rb
42
+ homepage: https://github.com/michaelherold/lexicoid-ruby
43
+ licenses:
44
+ - MIT
45
+ metadata:
46
+ bug_tracker_uri: https://github.com/michaelherold/lexicoid-ruby/issues
47
+ changelog_uri: https://github.com/michaelherold/lexicoid-ruby/blob/main/CHANGELOG.md
48
+ documentation_uri: https://rubydoc.info/gems/lexicoid-ruby/0.1.0
49
+ homepage_uri: https://github.com/michaelherold/lexicoid-ruby
50
+ rubygems_mfa_required: 'true'
51
+ source_code_uri: https://github.com/michaelherold/lexicoid-ruby
52
+ post_install_message:
53
+ rdoc_options: []
54
+ require_paths:
55
+ - lib
56
+ required_ruby_version: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: 3.0.0
61
+ required_rubygems_version: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ requirements: []
67
+ rubygems_version: 3.3.7
68
+ signing_key:
69
+ specification_version: 4
70
+ summary: Short and stable IDs from timestamps
71
+ test_files: []