Class: Ace::B36ts::Atoms::CompactIdEncoder
- Inherits:
-
Object
- Object
- Ace::B36ts::Atoms::CompactIdEncoder
- Extended by:
- FormatCodecs
- Defined in:
- lib/ace/b36ts/atoms/compact_id_encoder.rb
Overview
Encodes and decodes timestamps to/from variable-length Base36 compact IDs.
Supports 7 format types with varying precision and length:
-
2sec (6 chars, ~1.85s precision) - default
-
month (2 chars, month precision)
-
week (3 chars, week precision)
-
day (3 chars, day precision)
-
40min (4 chars, 40-minute block precision)
-
50ms (7 chars, ~50ms precision)
-
ms (8 chars, ~1.4ms precision)
Compact format design (6 Base36 digits):
-
Positions 1-2: Month offset from year_zero (0-1295 = 108 years of months)
-
Position 3: Day of month (0-30 maps to 1-31 calendar days)
-
Position 4: 40-minute hour block (0-35 = 36 blocks covering 24 hours)
-
Positions 5-6: Precision within 40-minute window (~1.85s precision)
Total capacity: 36^6 = 2,176,782,336 unique IDs over 108 years
Constant Summary collapse
- DEFAULT_YEAR_ZERO =
2000- DEFAULT_ALPHABET =
"0123456789abcdefghijklmnopqrstuvwxyz"- DEFAULT_ALPHABET_SET =
DEFAULT_ALPHABET.chars.to_set.freeze
- BLOCK_MINUTES =
40-minute block duration (36 blocks per day = 24 * 60 / 40)
40- BLOCK_SECONDS =
2400 seconds per block
BLOCK_MINUTES * 60
- PRECISION_DIVISOR =
Precision values within a 40-minute block 36^2 = 1296 combinations for 2400 seconds = ~1.85s precision
1296- PRECISION_DIVISOR_3 =
Additional precision for high-7 and high-8 formats
46_656- PRECISION_DIVISOR_4 =
36^3 for high-7 (~50ms precision)
1_679_616- MAX_MONTHS_OFFSET =
Maximum values for component validation
1295- MAX_DAY =
108 years * 12 months
30- MAX_BLOCK =
Calendar days 1-31 map to 0-30
35- MAX_PRECISION =
36 blocks per day (0-35)
1295
Class Method Summary collapse
-
.decode(compact_id, year_zero: DEFAULT_YEAR_ZERO, alphabet: DEFAULT_ALPHABET) ⇒ Time
Decode a 6-character compact ID to a Time object.
-
.decode_auto(encoded_id, year_zero: DEFAULT_YEAR_ZERO, alphabet: DEFAULT_ALPHABET) ⇒ Time
Decode a compact ID with automatic format detection.
-
.decode_path(path_string, year_zero: DEFAULT_YEAR_ZERO, alphabet: DEFAULT_ALPHABET) ⇒ Time
Decode a hierarchical split path into a Time object.
-
.decode_with_format(compact_id, format:, year_zero: DEFAULT_YEAR_ZERO, alphabet: DEFAULT_ALPHABET) ⇒ Time
Decode a compact ID to a Time object with specified format.
-
.detect_format(encoded_id, alphabet: DEFAULT_ALPHABET) ⇒ Symbol?
Detect the format of a compact ID string.
-
.encode(time, year_zero: DEFAULT_YEAR_ZERO, alphabet: DEFAULT_ALPHABET) ⇒ String
Encode a Time object to a 6-character compact ID.
-
.encode_sequence(time, count:, format:, year_zero: DEFAULT_YEAR_ZERO, alphabet: DEFAULT_ALPHABET) ⇒ Array<String>
Generate a sequence of sequential compact IDs starting from a time.
-
.encode_split(time, levels:, year_zero: DEFAULT_YEAR_ZERO, alphabet: DEFAULT_ALPHABET) ⇒ Hash
Encode a Time object into split components for hierarchical paths.
-
.encode_with_format(time, format:, year_zero: DEFAULT_YEAR_ZERO, alphabet: DEFAULT_ALPHABET) ⇒ String
Encode a Time object to a compact ID with specified format.
-
.increment_id(compact_id, format:, alphabet: DEFAULT_ALPHABET) ⇒ String
Increment a compact ID to the next sequential value.
-
.valid?(compact_id, alphabet: DEFAULT_ALPHABET) ⇒ Boolean
Validate a 6-character compact ID string (legacy method).
-
.valid_any_format?(compact_id, alphabet: DEFAULT_ALPHABET) ⇒ Boolean
Validate a compact ID string of any supported format.
Methods included from FormatCodecs
decode_2sec, decode_40min, decode_50ms, decode_day, decode_month, decode_ms, decode_week, encode_2sec, encode_40min, encode_50ms, encode_day, encode_month, encode_ms, encode_week, increment_2sec_id, increment_40min_id, increment_50ms_id, increment_day_id, increment_month_id, increment_ms_id, increment_week_id
Class Method Details
.decode(compact_id, year_zero: DEFAULT_YEAR_ZERO, alphabet: DEFAULT_ALPHABET) ⇒ Time
Decode a 6-character compact ID to a Time object
121 122 123 |
# File 'lib/ace/b36ts/atoms/compact_id_encoder.rb', line 121 def decode(compact_id, year_zero: DEFAULT_YEAR_ZERO, alphabet: DEFAULT_ALPHABET) decode_with_format(compact_id, format: :"2sec", year_zero: year_zero, alphabet: alphabet) end |
.decode_auto(encoded_id, year_zero: DEFAULT_YEAR_ZERO, alphabet: DEFAULT_ALPHABET) ⇒ Time
Decode a compact ID with automatic format detection
173 174 175 176 177 178 179 180 181 |
# File 'lib/ace/b36ts/atoms/compact_id_encoder.rb', line 173 def decode_auto(encoded_id, year_zero: DEFAULT_YEAR_ZERO, alphabet: DEFAULT_ALPHABET) format = detect_format(encoded_id, alphabet: alphabet) if format.nil? raise ArgumentError, "Cannot detect format for compact ID: #{encoded_id} (unsupported length or invalid characters)" end decode_with_format(encoded_id, format: format, year_zero: year_zero, alphabet: alphabet) end |
.decode_path(path_string, year_zero: DEFAULT_YEAR_ZERO, alphabet: DEFAULT_ALPHABET) ⇒ Time
Decode a hierarchical split path into a Time object
231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 |
# File 'lib/ace/b36ts/atoms/compact_id_encoder.rb', line 231 def decode_path(path_string, year_zero: DEFAULT_YEAR_ZERO, alphabet: DEFAULT_ALPHABET) raise ArgumentError, "Split path must be a string" unless path_string.is_a?(String) segments = path_string.split(/[\/\\:]+/).reject(&:empty?) full = segments.join # 7-char format: month(2) + week(1) + day(1) + block(1) + precision(2) = MMWDBRR # Strip week token (position 2) to get standard 6-char 2sec format: MMDBRRR if full.length == 7 full = full[0..1] + full[3..-1] elsif full.length != 6 raise ArgumentError, "Split path must resolve to 6 or 7 characters, got #{full.length}" end decode_2sec(full, year_zero: year_zero, alphabet: alphabet) end |
.decode_with_format(compact_id, format:, year_zero: DEFAULT_YEAR_ZERO, alphabet: DEFAULT_ALPHABET) ⇒ Time
Decode a compact ID to a Time object with specified format
133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 |
# File 'lib/ace/b36ts/atoms/compact_id_encoder.rb', line 133 def decode_with_format(compact_id, format:, year_zero: DEFAULT_YEAR_ZERO, alphabet: DEFAULT_ALPHABET) case format when :"2sec" decode_2sec(compact_id, year_zero: year_zero, alphabet: alphabet) when :month decode_month(compact_id, year_zero: year_zero, alphabet: alphabet) when :week decode_week(compact_id, year_zero: year_zero, alphabet: alphabet) when :day decode_day(compact_id, year_zero: year_zero, alphabet: alphabet) when :"40min" decode_40min(compact_id, year_zero: year_zero, alphabet: alphabet) when :"50ms" decode_50ms(compact_id, year_zero: year_zero, alphabet: alphabet) when :ms decode_ms(compact_id, year_zero: year_zero, alphabet: alphabet) else suggestion = suggest_format_name(format) msg = "Invalid format: #{format}. Must be one of #{FormatSpecs.all_formats.join(", ")}" msg += ". Did you mean '#{suggestion}'?" if suggestion raise ArgumentError, msg end end |
.detect_format(encoded_id, alphabet: DEFAULT_ALPHABET) ⇒ Symbol?
Detect the format of a compact ID string
162 163 164 |
# File 'lib/ace/b36ts/atoms/compact_id_encoder.rb', line 162 def detect_format(encoded_id, alphabet: DEFAULT_ALPHABET) FormatSpecs.detect_from_id(encoded_id, alphabet: alphabet) end |
.encode(time, year_zero: DEFAULT_YEAR_ZERO, alphabet: DEFAULT_ALPHABET) ⇒ String
Encode a Time object to a 6-character compact ID
76 77 78 |
# File 'lib/ace/b36ts/atoms/compact_id_encoder.rb', line 76 def encode(time, year_zero: DEFAULT_YEAR_ZERO, alphabet: DEFAULT_ALPHABET) encode_with_format(time, format: :"2sec", year_zero: year_zero, alphabet: alphabet) end |
.encode_sequence(time, count:, format:, year_zero: DEFAULT_YEAR_ZERO, alphabet: DEFAULT_ALPHABET) ⇒ Array<String>
Generate a sequence of sequential compact IDs starting from a time
289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 |
# File 'lib/ace/b36ts/atoms/compact_id_encoder.rb', line 289 def encode_sequence(time, count:, format:, year_zero: DEFAULT_YEAR_ZERO, alphabet: DEFAULT_ALPHABET) raise ArgumentError, "count must be greater than 0" if count <= 0 time = time.utc if time.respond_to?(:utc) # Generate the first ID first_id = encode_with_format(time, format: format, year_zero: year_zero, alphabet: alphabet) return [first_id] if count == 1 # Generate subsequent IDs by incrementing result = [first_id] current_id = first_id (count - 1).times do current_id = increment_id(current_id, format: format, alphabet: alphabet) result << current_id end result end |
.encode_split(time, levels:, year_zero: DEFAULT_YEAR_ZERO, alphabet: DEFAULT_ALPHABET) ⇒ Hash
Encode a Time object into split components for hierarchical paths
190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 |
# File 'lib/ace/b36ts/atoms/compact_id_encoder.rb', line 190 def encode_split(time, levels:, year_zero: DEFAULT_YEAR_ZERO, alphabet: DEFAULT_ALPHABET) time = time.utc if time.respond_to?(:utc) levels = normalize_split_levels(levels) validate_split_levels!(levels) full_compact = encode_2sec(time, year_zero: year_zero, alphabet: alphabet) components = { month: full_compact[0..1], day: full_compact[2], block: full_compact[3], precision: full_compact[4..5] } if levels.include?(:week) iso_year, iso_month, week_in_month = iso_week_month_and_number(time) iso_months_offset = calculate_months_offset_ym(iso_year, iso_month, year_zero) components[:month] = encode_value(iso_months_offset, 2, alphabet) week_token = encode_value(week_in_month + 30, 1, alphabet) end output = {} levels.each do |level| output[level] = (level == :week) ? week_token : components[level] end rest = split_rest_for(levels, full_compact) output[:rest] = rest path_components = levels.map { |level| output[level] } + [rest] output[:path] = path_components.join("/") output[:full] = path_components.join("") output end |
.encode_with_format(time, format:, year_zero: DEFAULT_YEAR_ZERO, alphabet: DEFAULT_ALPHABET) ⇒ String
Encode a Time object to a compact ID with specified format
88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 |
# File 'lib/ace/b36ts/atoms/compact_id_encoder.rb', line 88 def encode_with_format(time, format:, year_zero: DEFAULT_YEAR_ZERO, alphabet: DEFAULT_ALPHABET) time = time.utc if time.respond_to?(:utc) case format when :"2sec" encode_2sec(time, year_zero: year_zero, alphabet: alphabet) when :month encode_month(time, year_zero: year_zero, alphabet: alphabet) when :week encode_week(time, year_zero: year_zero, alphabet: alphabet) when :day encode_day(time, year_zero: year_zero, alphabet: alphabet) when :"40min" encode_40min(time, year_zero: year_zero, alphabet: alphabet) when :"50ms" encode_50ms(time, year_zero: year_zero, alphabet: alphabet) when :ms encode_ms(time, year_zero: year_zero, alphabet: alphabet) else suggestion = suggest_format_name(format) msg = "Invalid format: #{format}. Must be one of #{FormatSpecs.all_formats.join(", ")}" msg += ". Did you mean '#{suggestion}'?" if suggestion raise ArgumentError, msg end end |
.increment_id(compact_id, format:, alphabet: DEFAULT_ALPHABET) ⇒ String
Increment a compact ID to the next sequential value
Increments the smallest unit for the format, handling overflow cascade: ms → 50ms → 2sec → block → day → month
321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 |
# File 'lib/ace/b36ts/atoms/compact_id_encoder.rb', line 321 def increment_id(compact_id, format:, alphabet: DEFAULT_ALPHABET) base = alphabet.length id = compact_id.downcase case format when :month increment_month_id(id, alphabet, base) when :week increment_week_id(id, alphabet, base) when :day increment_day_id(id, alphabet, base) when :"40min" increment_40min_id(id, alphabet, base) when :"2sec" increment_2sec_id(id, alphabet, base) when :"50ms" increment_50ms_id(id, alphabet, base) when :ms increment_ms_id(id, alphabet, base) else raise ArgumentError, "Invalid format: #{format}" end end |
.valid?(compact_id, alphabet: DEFAULT_ALPHABET) ⇒ Boolean
Validate a 6-character compact ID string (legacy method)
NOTE: This method only validates the 6-character “2sec” compact format. For validating IDs of any format, use valid_any_format? instead.
261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 |
# File 'lib/ace/b36ts/atoms/compact_id_encoder.rb', line 261 def valid?(compact_id, alphabet: DEFAULT_ALPHABET) return false unless compact_id.is_a?(String) return false unless compact_id.length == 6 # Use Set for faster character validation (O(1) vs O(n)) alphabet_set = (alphabet == DEFAULT_ALPHABET) ? DEFAULT_ALPHABET_SET : alphabet.chars.to_set return false unless compact_id.downcase.chars.all? { |c| alphabet_set.include?(c) } # Also validate semantic ranges id = compact_id.downcase months_offset = decode_value(id[0..1], alphabet) day = decode_value(id[2], alphabet) block = decode_value(id[3], alphabet) precision = decode_value(id[4..5], alphabet) # Check component ranges (day must be 0-30 for calendar days 1-31) months_offset <= 1295 && day <= 30 && block <= 35 && precision <= 1295 end |
.valid_any_format?(compact_id, alphabet: DEFAULT_ALPHABET) ⇒ Boolean
Validate a compact ID string of any supported format
Supports all 7 formats: month (2 chars), week (3 chars), day (3 chars), 40min (4 chars), 2sec (6 chars), 50ms (7 chars), ms (8 chars).
353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 |
# File 'lib/ace/b36ts/atoms/compact_id_encoder.rb', line 353 def valid_any_format?(compact_id, alphabet: DEFAULT_ALPHABET) return false unless compact_id.is_a?(String) # Use Set for faster character validation (O(1) vs O(n)) alphabet_set = (alphabet == DEFAULT_ALPHABET) ? DEFAULT_ALPHABET_SET : alphabet.chars.to_set return false unless compact_id.downcase.chars.all? { |c| alphabet_set.include?(c) } # Try to detect format format = detect_format(compact_id, alphabet: alphabet) return false if format.nil? # Try to decode - if it succeeds, it's valid begin decode_with_format(compact_id, format: format, alphabet: alphabet) true rescue ArgumentError false end end |