lexicoid 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: 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: []