ace-b36ts 0.13.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 +7 -0
- data/.ace-defaults/b36ts/config.yml +13 -0
- data/.ace-defaults/nav/protocols/skill-sources/ace-b36ts.yml +19 -0
- data/.ace-defaults/nav/protocols/wfi-sources/ace-b36ts.yml +19 -0
- data/CHANGELOG.md +283 -0
- data/LICENSE +21 -0
- data/README.md +54 -0
- data/Rakefile +17 -0
- data/exe/ace-b36ts +14 -0
- data/handbook/agents/b36ts.ag.md +93 -0
- data/handbook/skills/as-b36ts/SKILL.md +20 -0
- data/handbook/workflow-instructions/b36ts.wf.md +127 -0
- data/lib/ace/b36ts/atoms/compact_id_encoder.rb +656 -0
- data/lib/ace/b36ts/atoms/format_codecs.rb +661 -0
- data/lib/ace/b36ts/atoms/format_specs.rb +178 -0
- data/lib/ace/b36ts/atoms/formats.rb +110 -0
- data/lib/ace/b36ts/cli/commands/config.rb +29 -0
- data/lib/ace/b36ts/cli/commands/decode.rb +47 -0
- data/lib/ace/b36ts/cli/commands/encode.rb +52 -0
- data/lib/ace/b36ts/cli.rb +71 -0
- data/lib/ace/b36ts/commands/config_command.rb +52 -0
- data/lib/ace/b36ts/commands/decode_command.rb +104 -0
- data/lib/ace/b36ts/commands/encode_command.rb +179 -0
- data/lib/ace/b36ts/molecules/config_resolver.rb +166 -0
- data/lib/ace/b36ts/version.rb +11 -0
- data/lib/ace/b36ts.rb +203 -0
- metadata +157 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "time"
|
|
5
|
+
|
|
6
|
+
module Ace
|
|
7
|
+
module B36ts
|
|
8
|
+
module Commands
|
|
9
|
+
# Command to encode a timestamp to a compact ID
|
|
10
|
+
#
|
|
11
|
+
# @example Usage
|
|
12
|
+
# EncodeCommand.execute("2025-01-06 12:30:00")
|
|
13
|
+
# # => "i50jj3"
|
|
14
|
+
#
|
|
15
|
+
class EncodeCommand
|
|
16
|
+
class << self
|
|
17
|
+
# Execute the encode command
|
|
18
|
+
#
|
|
19
|
+
# @param time_string [String] Time string to encode (various formats supported)
|
|
20
|
+
# @param options [Hash] Command options
|
|
21
|
+
# @option options [Integer] :year_zero Override year_zero config
|
|
22
|
+
# @option options [String] :format Output format
|
|
23
|
+
# @option options [Boolean] :quiet Suppress config summary output
|
|
24
|
+
# @return [Integer] Exit code (0 for success, 1 for error)
|
|
25
|
+
def execute(time_string, options = {})
|
|
26
|
+
time = parse_time(time_string)
|
|
27
|
+
config = Molecules::ConfigResolver.resolve(options)
|
|
28
|
+
|
|
29
|
+
display_config_summary("encode", config, options)
|
|
30
|
+
|
|
31
|
+
# Validate mutually exclusive options
|
|
32
|
+
if options[:split]
|
|
33
|
+
if options[:format]
|
|
34
|
+
raise ArgumentError, "--split and --format are mutually exclusive"
|
|
35
|
+
end
|
|
36
|
+
if options[:count]
|
|
37
|
+
raise ArgumentError, "--count and --split are mutually exclusive"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
levels = parse_split_levels(options[:split])
|
|
41
|
+
output = Atoms::CompactIdEncoder.encode_split(
|
|
42
|
+
time,
|
|
43
|
+
levels: levels,
|
|
44
|
+
year_zero: config[:year_zero],
|
|
45
|
+
alphabet: config[:alphabet]
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
if options[:path_only]
|
|
49
|
+
puts output[:path]
|
|
50
|
+
elsif options[:json]
|
|
51
|
+
puts JSON.generate(output.transform_keys(&:to_s))
|
|
52
|
+
else
|
|
53
|
+
display_split_output(levels, output)
|
|
54
|
+
end
|
|
55
|
+
else
|
|
56
|
+
# Get format from options or config (default: :"2sec")
|
|
57
|
+
# Normalize hyphens to underscores for CLI compatibility (e.g., high-8 -> high_8)
|
|
58
|
+
format = options[:format]
|
|
59
|
+
format = format.to_s.tr("-", "_").to_sym if format
|
|
60
|
+
format ||= config[:default_format]&.to_sym || :"2sec"
|
|
61
|
+
|
|
62
|
+
if options[:count]
|
|
63
|
+
count = options[:count].to_i
|
|
64
|
+
ids = Atoms::CompactIdEncoder.encode_sequence(
|
|
65
|
+
time,
|
|
66
|
+
count: count,
|
|
67
|
+
format: format,
|
|
68
|
+
year_zero: config[:year_zero],
|
|
69
|
+
alphabet: config[:alphabet]
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
if options[:json]
|
|
73
|
+
puts JSON.generate(ids)
|
|
74
|
+
else
|
|
75
|
+
ids.each { |id| puts id }
|
|
76
|
+
end
|
|
77
|
+
else
|
|
78
|
+
compact_id = Atoms::CompactIdEncoder.encode_with_format(
|
|
79
|
+
time,
|
|
80
|
+
format: format,
|
|
81
|
+
year_zero: config[:year_zero],
|
|
82
|
+
alphabet: config[:alphabet]
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
puts compact_id
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
0
|
|
89
|
+
rescue ArgumentError => e
|
|
90
|
+
warn "Error: #{e.message}"
|
|
91
|
+
raise
|
|
92
|
+
rescue => e
|
|
93
|
+
warn "Error encoding timestamp: #{e.message}"
|
|
94
|
+
warn e.backtrace.first(5).join("\n") if Ace::B36ts.debug?
|
|
95
|
+
raise
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
private
|
|
99
|
+
|
|
100
|
+
# Parse various time string formats
|
|
101
|
+
#
|
|
102
|
+
# @param time_string [String] Time string to parse
|
|
103
|
+
# @return [Time] Parsed time
|
|
104
|
+
# @raise [ArgumentError] If format is unrecognized
|
|
105
|
+
def parse_time(time_string)
|
|
106
|
+
return Time.now.utc if time_string.nil? || time_string.empty? || time_string == "now"
|
|
107
|
+
|
|
108
|
+
# Check for legacy timestamp format FIRST (YYYYMMDD-HHMMSS)
|
|
109
|
+
# Time.parse incorrectly parses this format (treats -HHMMSS as timezone offset)
|
|
110
|
+
if Atoms::Formats.timestamp?(time_string)
|
|
111
|
+
return Atoms::Formats.parse_timestamp(time_string)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
parsed = Time.parse(time_string)
|
|
115
|
+
return parsed if has_explicit_timezone?(time_string)
|
|
116
|
+
|
|
117
|
+
# Treat naïve timestamps as UTC to avoid local-time offsets on date-only
|
|
118
|
+
# inputs and systems with non-UTC defaults.
|
|
119
|
+
Time.utc(parsed.year, parsed.month, parsed.day, parsed.hour, parsed.min, parsed.sec, parsed.nsec)
|
|
120
|
+
rescue ArgumentError
|
|
121
|
+
raise ArgumentError, "Cannot parse time: #{time_string}"
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def has_explicit_timezone?(time_string)
|
|
125
|
+
value = time_string.to_s
|
|
126
|
+
return true if value.match?(%r{[+-]\d{2}:?\d{2}\b})
|
|
127
|
+
return true if value.match?(%r{(?:\A|[[:space:]])(?:Z|UTC|GMT)\b}i)
|
|
128
|
+
|
|
129
|
+
false
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Display configuration summary (unless quiet mode)
|
|
133
|
+
#
|
|
134
|
+
# @param command [String] Command name
|
|
135
|
+
# @param config [Hash] Resolved configuration
|
|
136
|
+
# @param options [Hash] Command options
|
|
137
|
+
def display_config_summary(command, config, options)
|
|
138
|
+
return if options[:quiet] || options[:path_only] || options[:json]
|
|
139
|
+
|
|
140
|
+
require "ace/core"
|
|
141
|
+
Ace::Core::Atoms::ConfigSummary.display(
|
|
142
|
+
command: command,
|
|
143
|
+
config: config,
|
|
144
|
+
defaults: Molecules::ConfigResolver::FALLBACK_DEFAULTS,
|
|
145
|
+
options: options,
|
|
146
|
+
quiet: false
|
|
147
|
+
)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Normalize split levels from string or array
|
|
151
|
+
#
|
|
152
|
+
# @param levels [String, Array<Symbol>] Split level list
|
|
153
|
+
# @return [Array<Symbol>] Normalized levels
|
|
154
|
+
def parse_split_levels(levels)
|
|
155
|
+
list = levels.is_a?(String) ? levels.split(",") : Array(levels)
|
|
156
|
+
list.map { |level| level.to_s.strip }
|
|
157
|
+
.reject(&:empty?)
|
|
158
|
+
.map(&:to_sym)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Display split output in key/value format
|
|
162
|
+
#
|
|
163
|
+
# @param levels [Array<Symbol>] Split levels in order
|
|
164
|
+
# @param output [Hash] Split output hash
|
|
165
|
+
def display_split_output(levels, output)
|
|
166
|
+
lines = []
|
|
167
|
+
levels.each do |level|
|
|
168
|
+
lines << "#{level}: #{output[level]}"
|
|
169
|
+
end
|
|
170
|
+
lines << "rest: #{output[:rest]}"
|
|
171
|
+
lines << "path: #{output[:path]}"
|
|
172
|
+
lines << "full: #{output[:full]}"
|
|
173
|
+
puts lines.join("\n")
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../atoms/format_specs"
|
|
4
|
+
|
|
5
|
+
module Ace
|
|
6
|
+
module B36ts
|
|
7
|
+
module Molecules
|
|
8
|
+
# Resolves configuration for ace-b36ts using the ace-config cascade.
|
|
9
|
+
#
|
|
10
|
+
# Configuration sources (in order of precedence):
|
|
11
|
+
# 1. Runtime options (passed directly to methods)
|
|
12
|
+
# 2. Project config (.ace/b36ts/config.yml)
|
|
13
|
+
# 3. User config (~/.ace/b36ts/config.yml)
|
|
14
|
+
# 4. Gem defaults (.ace-defaults/b36ts/config.yml)
|
|
15
|
+
#
|
|
16
|
+
# @example Get resolved configuration
|
|
17
|
+
# config = ConfigResolver.resolve
|
|
18
|
+
# config[:year_zero] # => 2000
|
|
19
|
+
# config[:alphabet] # => "0123456789abcdefghijklmnopqrstuvwxyz"
|
|
20
|
+
#
|
|
21
|
+
# @example Override with runtime options
|
|
22
|
+
# config = ConfigResolver.resolve(year_zero: 2025)
|
|
23
|
+
# config[:year_zero] # => 2025
|
|
24
|
+
#
|
|
25
|
+
module ConfigResolver
|
|
26
|
+
# Fallback defaults (used only if .ace-defaults files cannot be loaded)
|
|
27
|
+
FALLBACK_DEFAULTS = {
|
|
28
|
+
year_zero: 2000,
|
|
29
|
+
alphabet: "0123456789abcdefghijklmnopqrstuvwxyz",
|
|
30
|
+
default_format: :"2sec"
|
|
31
|
+
}.freeze
|
|
32
|
+
|
|
33
|
+
class << self
|
|
34
|
+
# Resolve configuration with optional overrides
|
|
35
|
+
#
|
|
36
|
+
# Uses Ace::Support::Config::Models::Config.wrap for proper merging per ADR-022.
|
|
37
|
+
#
|
|
38
|
+
# @param overrides [Hash] Runtime configuration overrides
|
|
39
|
+
# @return [Hash] Merged configuration
|
|
40
|
+
# @raise [ArgumentError] If configuration values are invalid
|
|
41
|
+
def resolve(overrides = {})
|
|
42
|
+
base_config = load_config
|
|
43
|
+
|
|
44
|
+
# Apply runtime overrides (symbolize keys, skip nil values)
|
|
45
|
+
symbolized_overrides = {}
|
|
46
|
+
overrides.each do |key, value|
|
|
47
|
+
symbolized_overrides[key.to_sym] = value unless value.nil?
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Merge base config with runtime overrides
|
|
51
|
+
config = Ace::Support::Config::Models::Config.wrap(
|
|
52
|
+
base_config,
|
|
53
|
+
symbolized_overrides,
|
|
54
|
+
source: "ace-b36ts"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# Validate the merged configuration
|
|
58
|
+
validate_config!(config)
|
|
59
|
+
|
|
60
|
+
config
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Get the year_zero value from configuration
|
|
64
|
+
#
|
|
65
|
+
# @param override [Integer, nil] Optional runtime override
|
|
66
|
+
# @return [Integer] The year_zero value
|
|
67
|
+
def year_zero(override = nil)
|
|
68
|
+
override || resolve[:year_zero]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Get the alphabet value from configuration
|
|
72
|
+
#
|
|
73
|
+
# @param override [String, nil] Optional runtime override
|
|
74
|
+
# @return [String] The alphabet value
|
|
75
|
+
def alphabet(override = nil)
|
|
76
|
+
override || resolve[:alphabet]
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Get the default_format value from configuration
|
|
80
|
+
#
|
|
81
|
+
# @param override [Symbol, String, nil] Optional runtime override
|
|
82
|
+
# @return [Symbol] The default_format value (always a symbol)
|
|
83
|
+
def default_format(override = nil)
|
|
84
|
+
value = override || resolve[:default_format]
|
|
85
|
+
value.is_a?(String) ? value.to_sym : value
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Reset cached configuration (useful for testing)
|
|
89
|
+
def reset!
|
|
90
|
+
@config = nil
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
# Validate configuration values
|
|
96
|
+
#
|
|
97
|
+
# @param config [Hash] Configuration to validate
|
|
98
|
+
# @raise [ArgumentError] If configuration values are invalid
|
|
99
|
+
def validate_config!(config)
|
|
100
|
+
alphabet = config[:alphabet]
|
|
101
|
+
year_zero = config[:year_zero]
|
|
102
|
+
default_format = config[:default_format]
|
|
103
|
+
|
|
104
|
+
unless alphabet.is_a?(String) && alphabet.length == 36
|
|
105
|
+
raise ArgumentError, "alphabet must be exactly 36 characters, got #{alphabet&.length || "nil"}"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Verify all characters in the alphabet are unique
|
|
109
|
+
unless alphabet.chars.uniq.length == 36
|
|
110
|
+
raise ArgumentError, "alphabet must contain 36 unique characters (duplicates found)"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
unless year_zero.is_a?(Integer) && year_zero.between?(1900, 2100)
|
|
114
|
+
raise ArgumentError, "year_zero must be between 1900-2100, got #{year_zero.inspect}"
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Validate default_format is a supported format
|
|
118
|
+
format_sym = default_format.is_a?(String) ? default_format.to_sym : default_format
|
|
119
|
+
unless format_sym.nil? || Ace::B36ts::Atoms::FormatSpecs.valid_format?(format_sym)
|
|
120
|
+
raise ArgumentError, "default_format must be one of #{Ace::B36ts::Atoms::FormatSpecs.all_formats.join(", ")}, got #{default_format.inspect}"
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Load configuration using ace-config cascade
|
|
125
|
+
#
|
|
126
|
+
# @return [Hash] Loaded configuration with symbolized keys
|
|
127
|
+
def load_config
|
|
128
|
+
@config ||= begin
|
|
129
|
+
gem_root = Gem.loaded_specs["ace-b36ts"]&.gem_dir ||
|
|
130
|
+
File.expand_path("../../../..", __dir__)
|
|
131
|
+
|
|
132
|
+
resolver = Ace::Support::Config.create(
|
|
133
|
+
config_dir: ".ace",
|
|
134
|
+
defaults_dir: ".ace-defaults",
|
|
135
|
+
gem_path: gem_root
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
loaded_config = resolver.resolve_namespace("b36ts")
|
|
139
|
+
# loaded_config.data is already namespaced (no need to fetch "b36ts" key again)
|
|
140
|
+
user_config = symbolize_keys(loaded_config.data)
|
|
141
|
+
|
|
142
|
+
# Merge user config with fallback defaults (user values take precedence)
|
|
143
|
+
FALLBACK_DEFAULTS.merge(user_config)
|
|
144
|
+
rescue Psych::SyntaxError => e
|
|
145
|
+
config_path = File.join(Dir.pwd, ".ace", "b36ts", "config.yml")
|
|
146
|
+
warn "Error: Failed to parse #{config_path}: #{e.message}" if Ace::B36ts.debug?
|
|
147
|
+
warn " Check YAML syntax at line #{e.line}, column #{e.column}" if Ace::B36ts.debug? && e.line
|
|
148
|
+
FALLBACK_DEFAULTS.dup
|
|
149
|
+
rescue => e
|
|
150
|
+
warn "Warning: Could not load ace-b36ts config: #{e.message}" if Ace::B36ts.debug?
|
|
151
|
+
FALLBACK_DEFAULTS.dup
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Convert string keys to symbols
|
|
156
|
+
#
|
|
157
|
+
# @param hash [Hash] Hash with string or symbol keys
|
|
158
|
+
# @return [Hash] Hash with symbol keys
|
|
159
|
+
def symbolize_keys(hash)
|
|
160
|
+
hash.transform_keys(&:to_sym)
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module B36ts
|
|
5
|
+
# Version 0.2.0: Gem renamed from ace-timestamp to ace-support-timestamp
|
|
6
|
+
# Namespace changed from Ace::Timestamp to Ace::Support::Timestamp
|
|
7
|
+
# Version 0.6.0: Gem renamed from ace-support-timestamp to ace-b36ts
|
|
8
|
+
# Namespace changed from Ace::Support::Timestamp to Ace::B36ts
|
|
9
|
+
VERSION = "0.13.0"
|
|
10
|
+
end
|
|
11
|
+
end
|
data/lib/ace/b36ts.rb
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "b36ts/version"
|
|
4
|
+
|
|
5
|
+
# Load ace-config for configuration cascade management
|
|
6
|
+
require "ace/support/config"
|
|
7
|
+
|
|
8
|
+
# Atoms
|
|
9
|
+
require_relative "b36ts/atoms/compact_id_encoder"
|
|
10
|
+
require_relative "b36ts/atoms/formats"
|
|
11
|
+
require_relative "b36ts/atoms/format_specs"
|
|
12
|
+
|
|
13
|
+
# Molecules
|
|
14
|
+
require_relative "b36ts/molecules/config_resolver"
|
|
15
|
+
|
|
16
|
+
# Commands
|
|
17
|
+
require_relative "b36ts/commands/encode_command"
|
|
18
|
+
require_relative "b36ts/commands/decode_command"
|
|
19
|
+
require_relative "b36ts/commands/config_command"
|
|
20
|
+
|
|
21
|
+
# CLI
|
|
22
|
+
require_relative "b36ts/cli"
|
|
23
|
+
|
|
24
|
+
module Ace
|
|
25
|
+
# B36ts module providing Base36 compact ID generation for timestamps.
|
|
26
|
+
#
|
|
27
|
+
# This module provides a 6-character Base36 encoding for timestamps that
|
|
28
|
+
# replaces traditional 14-character timestamp formats (YYYYMMDD-HHMMSS).
|
|
29
|
+
#
|
|
30
|
+
# @example Quick encoding
|
|
31
|
+
# Ace::B36ts.encode(Time.now)
|
|
32
|
+
# # => "i50jj3"
|
|
33
|
+
#
|
|
34
|
+
# @example Quick decoding
|
|
35
|
+
# Ace::B36ts.decode("i50jj3")
|
|
36
|
+
# # => 2025-01-06 12:30:00 UTC
|
|
37
|
+
#
|
|
38
|
+
# @example Format detection
|
|
39
|
+
# Ace::B36ts.detect_format("i50jj3") # => :"2sec"
|
|
40
|
+
# Ace::B36ts.detect_format("20250106-123000") # => :timestamp
|
|
41
|
+
#
|
|
42
|
+
module B36ts
|
|
43
|
+
class Error < StandardError; end
|
|
44
|
+
|
|
45
|
+
# Check if debug mode is enabled
|
|
46
|
+
# @return [Boolean] True if debug mode is enabled
|
|
47
|
+
def self.debug?
|
|
48
|
+
ENV["ACE_DEBUG"] == "1" || ENV["DEBUG"] == "1"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Encode a Time object to a compact ID (convenience method)
|
|
52
|
+
#
|
|
53
|
+
# @param time [Time] The time to encode
|
|
54
|
+
# @param format [Symbol, nil] Output format (default: :"2sec")
|
|
55
|
+
# Supported formats: :month (2 chars), :week (3 chars), :day (3 chars),
|
|
56
|
+
# :"40min" (4 chars), :"2sec" (6 chars), :"50ms" (7 chars), :ms (8 chars)
|
|
57
|
+
# @param year_zero [Integer, nil] Optional year_zero override
|
|
58
|
+
# @return [String] Compact ID (length varies by format)
|
|
59
|
+
def self.encode(time, format: nil, year_zero: nil)
|
|
60
|
+
config = Molecules::ConfigResolver.resolve(year_zero: year_zero)
|
|
61
|
+
effective_format = format || config[:default_format]&.to_sym || :"2sec"
|
|
62
|
+
|
|
63
|
+
Atoms::CompactIdEncoder.encode_with_format(
|
|
64
|
+
time,
|
|
65
|
+
format: effective_format,
|
|
66
|
+
year_zero: config[:year_zero],
|
|
67
|
+
alphabet: config[:alphabet]
|
|
68
|
+
)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Decode a compact ID to a Time object (convenience method)
|
|
72
|
+
#
|
|
73
|
+
# @param compact_id [String] The compact ID to decode
|
|
74
|
+
# @param format [Symbol, nil] Format of the ID (default: :"2sec" for 6-char, or auto-detect)
|
|
75
|
+
# @param year_zero [Integer, nil] Optional year_zero override
|
|
76
|
+
# @return [Time] Decoded time in UTC
|
|
77
|
+
def self.decode(compact_id, format: nil, year_zero: nil)
|
|
78
|
+
config = Molecules::ConfigResolver.resolve(year_zero: year_zero)
|
|
79
|
+
|
|
80
|
+
if format
|
|
81
|
+
Atoms::CompactIdEncoder.decode_with_format(
|
|
82
|
+
compact_id,
|
|
83
|
+
format: format,
|
|
84
|
+
year_zero: config[:year_zero],
|
|
85
|
+
alphabet: config[:alphabet]
|
|
86
|
+
)
|
|
87
|
+
else
|
|
88
|
+
# Default to 2sec format for backward compatibility with 6-char IDs
|
|
89
|
+
Atoms::CompactIdEncoder.decode(
|
|
90
|
+
compact_id,
|
|
91
|
+
year_zero: config[:year_zero],
|
|
92
|
+
alphabet: config[:alphabet]
|
|
93
|
+
)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Decode a compact ID with automatic format detection (convenience method)
|
|
98
|
+
#
|
|
99
|
+
# Automatically detects the format based on ID length and value ranges:
|
|
100
|
+
# - 2 chars: month format
|
|
101
|
+
# - 3 chars: day or week format (auto-detected by 3rd char value)
|
|
102
|
+
# - 4 chars: 40min format
|
|
103
|
+
# - 6 chars: 2sec format
|
|
104
|
+
# - 7 chars: 50ms format
|
|
105
|
+
# - 8 chars: ms format
|
|
106
|
+
#
|
|
107
|
+
# @param compact_id [String] The compact ID to decode (2-8 characters)
|
|
108
|
+
# @param year_zero [Integer, nil] Optional year_zero override
|
|
109
|
+
# @return [Time] Decoded time in UTC
|
|
110
|
+
# @raise [ArgumentError] If format cannot be detected
|
|
111
|
+
def self.decode_auto(compact_id, year_zero: nil)
|
|
112
|
+
config = Molecules::ConfigResolver.resolve(year_zero: year_zero)
|
|
113
|
+
Atoms::CompactIdEncoder.decode_auto(
|
|
114
|
+
compact_id,
|
|
115
|
+
year_zero: config[:year_zero],
|
|
116
|
+
alphabet: config[:alphabet]
|
|
117
|
+
)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Encode a Time object into split components for hierarchical paths
|
|
121
|
+
#
|
|
122
|
+
# @param time [Time] The time to encode
|
|
123
|
+
# @param levels [Array<Symbol>, String] Split levels (month, week, day, block)
|
|
124
|
+
# @param path_only [Boolean] Return only the path string
|
|
125
|
+
# @param year_zero [Integer, nil] Optional year_zero override
|
|
126
|
+
# @return [Hash, String] Split component hash or path string
|
|
127
|
+
def self.encode_split(time, levels:, path_only: false, year_zero: nil)
|
|
128
|
+
config = Molecules::ConfigResolver.resolve(year_zero: year_zero)
|
|
129
|
+
result = Atoms::CompactIdEncoder.encode_split(
|
|
130
|
+
time,
|
|
131
|
+
levels: levels,
|
|
132
|
+
year_zero: config[:year_zero],
|
|
133
|
+
alphabet: config[:alphabet]
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
path_only ? result[:path] : result
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Decode a hierarchical split path into a Time object
|
|
140
|
+
#
|
|
141
|
+
# @param path_string [String] Split path string
|
|
142
|
+
# @param year_zero [Integer, nil] Optional year_zero override
|
|
143
|
+
# @return [Time] Decoded time in UTC
|
|
144
|
+
def self.decode_path(path_string, year_zero: nil)
|
|
145
|
+
config = Molecules::ConfigResolver.resolve(year_zero: year_zero)
|
|
146
|
+
Atoms::CompactIdEncoder.decode_path(
|
|
147
|
+
path_string,
|
|
148
|
+
year_zero: config[:year_zero],
|
|
149
|
+
alphabet: config[:alphabet]
|
|
150
|
+
)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Validate if a string is a valid 6-character compact ID (legacy method)
|
|
154
|
+
#
|
|
155
|
+
# NOTE: This method only validates 6-character "2sec" format IDs.
|
|
156
|
+
# For validating IDs of any format, use valid_any_format? instead.
|
|
157
|
+
#
|
|
158
|
+
# @param compact_id [String] The string to validate
|
|
159
|
+
# @return [Boolean] True if valid 6-char compact ID format
|
|
160
|
+
# @see valid_any_format? for validating all format lengths
|
|
161
|
+
def self.valid?(compact_id)
|
|
162
|
+
Atoms::CompactIdEncoder.valid?(compact_id)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Validate if a string is a valid compact ID of any format
|
|
166
|
+
#
|
|
167
|
+
# Supports all 7 formats: month (2 chars), week (3 chars), day (3 chars),
|
|
168
|
+
# 40min (4 chars), 2sec (6 chars), 50ms (7 chars), ms (8 chars).
|
|
169
|
+
#
|
|
170
|
+
# @param compact_id [String] The string to validate (2-8 characters)
|
|
171
|
+
# @return [Boolean] True if valid compact ID of any format
|
|
172
|
+
def self.valid_any_format?(compact_id)
|
|
173
|
+
Atoms::CompactIdEncoder.valid_any_format?(compact_id)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Detect the format of a timestamp string
|
|
177
|
+
#
|
|
178
|
+
# @param value [String] The timestamp string
|
|
179
|
+
# @return [Symbol, nil] :"2sec", :timestamp, or nil
|
|
180
|
+
def self.detect_format(value)
|
|
181
|
+
Atoms::Formats.detect(value)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Generate a compact ID for the current time (convenience method)
|
|
185
|
+
#
|
|
186
|
+
# @param year_zero [Integer, nil] Optional year_zero override
|
|
187
|
+
# @return [String] 6-character compact ID for current time
|
|
188
|
+
def self.now(year_zero: nil)
|
|
189
|
+
encode(Time.now.utc, year_zero: year_zero)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Load configuration using ace-config cascade
|
|
193
|
+
# @return [Hash] Configuration hash
|
|
194
|
+
def self.config
|
|
195
|
+
Molecules::ConfigResolver.resolve
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Reset configuration cache (useful for testing)
|
|
199
|
+
def self.reset_config!
|
|
200
|
+
Molecules::ConfigResolver.reset!
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|