zone 0.1.1 → 0.1.2
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 +4 -4
- data/CHANGELOG.md +77 -0
- data/README.md +122 -27
- data/TEST_PLAN.md +911 -0
- data/completions/README.md +126 -0
- data/completions/_zone +89 -0
- data/docs/user-experience-review.md +150 -0
- data/exe/zone +2 -265
- data/lib/zone/cli.rb +64 -0
- data/lib/zone/colors.rb +179 -0
- data/lib/zone/field.rb +67 -0
- data/lib/zone/field_line.rb +97 -0
- data/lib/zone/field_mapping.rb +51 -0
- data/lib/zone/input.rb +52 -0
- data/lib/zone/logging.rb +38 -0
- data/lib/zone/options.rb +142 -0
- data/lib/zone/output.rb +39 -0
- data/lib/zone/pattern.rb +59 -0
- data/lib/zone/timestamp.rb +138 -0
- data/lib/zone/timestamp_patterns.rb +169 -0
- data/lib/zone/transform.rb +69 -0
- data/lib/zone/version.rb +1 -1
- data/lib/zone.rb +45 -1
- data/todo.md +85 -0
- metadata +19 -1
data/lib/zone/pattern.rb
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'timestamp_patterns'
|
|
4
|
+
|
|
5
|
+
module Zone
|
|
6
|
+
module Pattern
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def process(input, output, transformation, logger)
|
|
10
|
+
input.each_line do |line|
|
|
11
|
+
process_line(line, input.from_arguments?, output, transformation, logger)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def process_line(line, from_arguments, output, transformation, logger)
|
|
16
|
+
case line
|
|
17
|
+
in ""
|
|
18
|
+
logger.warn("Could not parse time from empty line") if from_arguments
|
|
19
|
+
output.puts(line) unless from_arguments
|
|
20
|
+
else
|
|
21
|
+
result = replace_timestamps(line, output, transformation, logger)
|
|
22
|
+
|
|
23
|
+
case [result == line, from_arguments]
|
|
24
|
+
in [true, true]
|
|
25
|
+
parse_as_argument(line, output, transformation)
|
|
26
|
+
in [true, false]
|
|
27
|
+
output.puts(line)
|
|
28
|
+
in [false, _]
|
|
29
|
+
output.puts(result)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
private_class_method :process_line
|
|
34
|
+
|
|
35
|
+
def replace_timestamps(line, output, transformation, logger)
|
|
36
|
+
TimestampPatterns.replace_all(line, logger: logger) do |match, _pattern|
|
|
37
|
+
transform_timestamp(match, output, transformation, logger)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
private_class_method :replace_timestamps
|
|
41
|
+
|
|
42
|
+
def transform_timestamp(match, output, transformation, logger)
|
|
43
|
+
formatted = transformation.call(match)
|
|
44
|
+
output.colorize_timestamp(formatted)
|
|
45
|
+
rescue StandardError => e
|
|
46
|
+
logger.warn("Could not parse time: #{e.message}")
|
|
47
|
+
match
|
|
48
|
+
end
|
|
49
|
+
private_class_method :transform_timestamp
|
|
50
|
+
|
|
51
|
+
def parse_as_argument(text, output, transformation)
|
|
52
|
+
formatted = transformation.call(text)
|
|
53
|
+
raise ArgumentError, "Could not parse time '#{text}'" if formatted.nil?
|
|
54
|
+
|
|
55
|
+
output.puts(output.colorize_timestamp(formatted))
|
|
56
|
+
end
|
|
57
|
+
private_class_method :parse_as_argument
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'time'
|
|
4
|
+
require 'date'
|
|
5
|
+
|
|
6
|
+
module Zone
|
|
7
|
+
class Timestamp
|
|
8
|
+
attr_reader :time, :zone
|
|
9
|
+
|
|
10
|
+
def self.parse(input)
|
|
11
|
+
time = case input
|
|
12
|
+
in Time
|
|
13
|
+
input
|
|
14
|
+
in DateTime
|
|
15
|
+
input.to_time
|
|
16
|
+
in Date
|
|
17
|
+
input.to_time
|
|
18
|
+
in /^[0-9\.]+$/
|
|
19
|
+
parse_unix(input)
|
|
20
|
+
in /^(?<amount>[0-9\.]+) (?<unit>second|minute|hour|day|week|month|year|decade)s? (?<direction>ago|from now)$/
|
|
21
|
+
parse_relative($~)
|
|
22
|
+
in /^(?<dow>[A-Z][a-z]{2}) (?<mon>[A-Z][a-z]{2}) (?<day>\d{1,2}) (?<time>\d{2}:\d{2}:\d{2}) (?<year>\d{4}) (?<offset>[+-]\d{4})$/
|
|
23
|
+
# Git log format: "Fri Nov 14 14:54:35 2025 -0500" or "Wed Nov 5 11:24:19 2025 -0500"
|
|
24
|
+
# Reorder to: "Fri Nov 14 14:54:35 -0500 2025" for DateTime.parse
|
|
25
|
+
parse_git_log($~)
|
|
26
|
+
else
|
|
27
|
+
DateTime.parse(input).to_time
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
new(time)
|
|
31
|
+
rescue StandardError
|
|
32
|
+
raise ArgumentError, "Could not parse time '#{input}'"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def initialize(time, zone: nil)
|
|
36
|
+
@time = time
|
|
37
|
+
@zone = zone
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def in_zone(zone_name)
|
|
41
|
+
tz = Zone.find(zone_name)
|
|
42
|
+
|
|
43
|
+
raise ArgumentError, "Could not find timezone '#{zone_name}'" if tz.nil?
|
|
44
|
+
|
|
45
|
+
converted = tz.to_local(@time)
|
|
46
|
+
|
|
47
|
+
self.class.new(
|
|
48
|
+
converted,
|
|
49
|
+
zone: zone_name
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def in_utc
|
|
54
|
+
self.class.new(
|
|
55
|
+
@time.utc,
|
|
56
|
+
zone: 'UTC'
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def in_local
|
|
61
|
+
self.class.new(
|
|
62
|
+
@time.localtime,
|
|
63
|
+
zone: 'local'
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def to_iso8601
|
|
68
|
+
@time.utc_offset.zero? ? @time.strftime('%Y-%m-%dT%H:%M:%SZ') : @time.iso8601
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def to_unix
|
|
72
|
+
@time.to_i
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def to_pretty(style = 1)
|
|
76
|
+
case style
|
|
77
|
+
when 1
|
|
78
|
+
@time.strftime('%b %d, %Y - %l:%M %p %Z')
|
|
79
|
+
when 2
|
|
80
|
+
@time.strftime('%b %d, %Y - %H:%M %Z')
|
|
81
|
+
when 3
|
|
82
|
+
@time.strftime('%Y-%m-%d %H:%M %Z')
|
|
83
|
+
else
|
|
84
|
+
raise ArgumentError, "Invalid pretty style '#{style}' (must be 1, 2, or 3)"
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def strftime(format)
|
|
89
|
+
@time.strftime(format)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
private
|
|
93
|
+
|
|
94
|
+
def self.parse_unix(str)
|
|
95
|
+
precision = str.length - 10
|
|
96
|
+
time_float = str.to_f / 10**precision
|
|
97
|
+
Time.at(time_float)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def self.parse_relative(match_data)
|
|
101
|
+
match_data => { amount:, unit:, direction: }
|
|
102
|
+
|
|
103
|
+
seconds = case unit
|
|
104
|
+
in 'second'
|
|
105
|
+
amount.to_i
|
|
106
|
+
in 'minute'
|
|
107
|
+
amount.to_i * 60
|
|
108
|
+
in 'hour'
|
|
109
|
+
amount.to_i * 3600
|
|
110
|
+
in 'day'
|
|
111
|
+
amount.to_i * 86400
|
|
112
|
+
in 'week'
|
|
113
|
+
amount.to_i * 604800
|
|
114
|
+
in 'month'
|
|
115
|
+
amount.to_i * 2592000
|
|
116
|
+
in 'year'
|
|
117
|
+
amount.to_i * 31536000
|
|
118
|
+
in 'decade'
|
|
119
|
+
amount.to_i * 315360000
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
case direction
|
|
123
|
+
in 'ago'
|
|
124
|
+
Time.now - seconds
|
|
125
|
+
in 'from now'
|
|
126
|
+
Time.now + seconds
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def self.parse_git_log(match_data)
|
|
131
|
+
# Git log format: "Fri Nov 14 14:54:35 2025 -0500"
|
|
132
|
+
# Reorder to parseable format: "Fri Nov 14 14:54:35 -0500 2025"
|
|
133
|
+
match_data => { dow:, mon:, day:, time:, year:, offset: }
|
|
134
|
+
reordered = "#{dow} #{mon} #{day} #{time} #{offset} #{year}"
|
|
135
|
+
DateTime.parse(reordered).to_time
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Zone
|
|
4
|
+
#
|
|
5
|
+
# Pattern matching for timestamps in arbitrary text.
|
|
6
|
+
#
|
|
7
|
+
# Provides regex patterns and utilities for finding and replacing
|
|
8
|
+
# timestamps embedded in unstructured text.
|
|
9
|
+
#
|
|
10
|
+
module TimestampPatterns
|
|
11
|
+
# Patterns are prefixed with P## to define priority order (most specific first).
|
|
12
|
+
# To add a new pattern, simply define a new Regexp constant with appropriate priority number.
|
|
13
|
+
# It will be automatically included in pattern matching.
|
|
14
|
+
|
|
15
|
+
# ISO 8601 with timezone offset (e.g., 2025-01-15T10:30:00+09:00)
|
|
16
|
+
P01_ISO8601_WITH_TZ = /\b\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?[+-]\d{2}:\d{2}\b/
|
|
17
|
+
|
|
18
|
+
# ISO 8601 with Zulu (UTC) timezone (e.g., 2025-01-15T10:30:00Z)
|
|
19
|
+
P02_ISO8601_ZULU = /\b\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z\b/
|
|
20
|
+
|
|
21
|
+
# ISO 8601 with space separator and offset (e.g., 2025-01-15 10:30:00 -0500) - Ruby Time.now.to_s format
|
|
22
|
+
P03_ISO8601_SPACE_WITH_OFFSET = /\b\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:\.\d+)? [+-]\d{4}\b/
|
|
23
|
+
|
|
24
|
+
# ISO 8601 with space separator, no timezone (e.g., 2025-01-15 10:30:00) - common SQL/database format
|
|
25
|
+
# Use negative lookahead to ensure not followed by timezone offset
|
|
26
|
+
P04_ISO8601_SPACE = /\b\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:\.\d+)?(?! [+-]\d)/
|
|
27
|
+
|
|
28
|
+
# Zone pretty1 format: 12hr with AM/PM (e.g., "Jan 15, 2025 - 10:30 AM UTC" or "Jan 15, 2025 - 1:30 AM UTC")
|
|
29
|
+
P05_PRETTY1_12HR = /\b[A-Z][a-z]{2} \d{2}, \d{4} - \s?\d{1,2}:\d{2} [AP]M [A-Z]{3,4}\b/
|
|
30
|
+
|
|
31
|
+
# Zone pretty2 format: 24hr without AM/PM (e.g., "Jan 15, 2025 - 10:30 UTC")
|
|
32
|
+
P06_PRETTY2_24HR = /\b[A-Z][a-z]{2} \d{2}, \d{4} - \d{2}:\d{2} [A-Z]{3,4}\b/
|
|
33
|
+
|
|
34
|
+
# Zone pretty3 format: ISO-style compact (e.g., "2025-01-15 10:30 UTC")
|
|
35
|
+
P07_PRETTY3_ISO = /\b\d{4}-\d{2}-\d{2} \d{2}:\d{2} [A-Z]{3,4}\b/
|
|
36
|
+
|
|
37
|
+
# Unix timestamp (10 digits, 2001-2036, e.g., 1736937000)
|
|
38
|
+
# Matches timestamps starting with 1 (2001-2033) or 20-21 (2033-2039)
|
|
39
|
+
# Avoids false positives from phone numbers, order IDs, hex strings (commit hashes), etc.
|
|
40
|
+
# Uses hex boundary check to prevent matching within commit hashes
|
|
41
|
+
P08_UNIX_TIMESTAMP = /(?<![0-9a-fA-F])(?:1\d{9}|2[0-1]\d{8})(?![0-9a-fA-F])/
|
|
42
|
+
|
|
43
|
+
# Relative time expressions (e.g., "5 hours ago", "3 days from now")
|
|
44
|
+
P09_RELATIVE_TIME = /\b\d+\s+(?:second|minute|hour|day|week|month|year)s?\s+(?:ago|from now)\b/i
|
|
45
|
+
|
|
46
|
+
# Git log format (e.g., "Fri Nov 14 23:48:24 2025 +0000", "Wed Nov 5 11:24:19 2025 -0500")
|
|
47
|
+
P10_GIT_LOG = /\b[A-Z][a-z]{2} [A-Z][a-z]{2} \d{1,2} \d{2}:\d{2}:\d{2} \d{4} [+-]\d{4}\b/
|
|
48
|
+
|
|
49
|
+
# Date command output format (e.g., "Wed Nov 12 19:13:17 UTC 2025")
|
|
50
|
+
P11_DATE_COMMAND = /\b[A-Z][a-z]{2} [A-Z][a-z]{2} \d{1,2} \d{2}:\d{2}:\d{2} [A-Z]{3,4} \d{4}\b/
|
|
51
|
+
|
|
52
|
+
module_function
|
|
53
|
+
|
|
54
|
+
#
|
|
55
|
+
# Returns all timestamp patterns in priority order.
|
|
56
|
+
#
|
|
57
|
+
# @return [Array<Regexp>]
|
|
58
|
+
# All Regexp constants defined in this module, sorted by P## prefix
|
|
59
|
+
#
|
|
60
|
+
def patterns
|
|
61
|
+
constants
|
|
62
|
+
.select { |c| c.to_s.match?(/^P\d+_/) }
|
|
63
|
+
.sort_by { |c| c.to_s[/^P(\d+)_/, 1].to_i }
|
|
64
|
+
.map { |c| const_get(c) }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
#
|
|
68
|
+
# Check if text contains any timestamp patterns.
|
|
69
|
+
#
|
|
70
|
+
# @param [String] text
|
|
71
|
+
# The text to check
|
|
72
|
+
#
|
|
73
|
+
# @return [Boolean]
|
|
74
|
+
# true if text matches any timestamp pattern
|
|
75
|
+
#
|
|
76
|
+
def match?(text)
|
|
77
|
+
patterns.any? { |pattern| pattern.match?(text) }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
#
|
|
81
|
+
# Replace all timestamp patterns in text.
|
|
82
|
+
#
|
|
83
|
+
# @param [String] text
|
|
84
|
+
# The text to search for timestamps
|
|
85
|
+
#
|
|
86
|
+
# @param [Logger, nil] logger
|
|
87
|
+
# Optional logger for debug output
|
|
88
|
+
#
|
|
89
|
+
# @yield [match, pattern]
|
|
90
|
+
# Block receives each matched timestamp string and its pattern
|
|
91
|
+
#
|
|
92
|
+
# @yieldparam [String] match
|
|
93
|
+
# The matched timestamp string
|
|
94
|
+
#
|
|
95
|
+
# @yieldparam [Regexp] pattern
|
|
96
|
+
# The pattern that matched
|
|
97
|
+
#
|
|
98
|
+
# @yieldreturn [String]
|
|
99
|
+
# The replacement string
|
|
100
|
+
#
|
|
101
|
+
# @return [String]
|
|
102
|
+
# Text with all timestamps replaced
|
|
103
|
+
#
|
|
104
|
+
# @example
|
|
105
|
+
# text = "Logged in at 2025-01-15T10:30:00Z"
|
|
106
|
+
# result = replace_all(text) do |match, pattern|
|
|
107
|
+
# timestamp = Timestamp.parse(match)
|
|
108
|
+
# timestamp.to_pretty
|
|
109
|
+
# end
|
|
110
|
+
# # => "Logged in at Jan 15, 2025 - 10:30 AM UTC"
|
|
111
|
+
#
|
|
112
|
+
def replace_all(text, logger: nil)
|
|
113
|
+
result = text.dup
|
|
114
|
+
matches = 0
|
|
115
|
+
|
|
116
|
+
patterns.each do |pattern|
|
|
117
|
+
result.gsub!(pattern) do |match|
|
|
118
|
+
next match unless valid_timestamp?(match, pattern)
|
|
119
|
+
|
|
120
|
+
matches += 1
|
|
121
|
+
|
|
122
|
+
begin
|
|
123
|
+
yield(match, pattern)
|
|
124
|
+
rescue StandardError => e
|
|
125
|
+
logger&.debug("Failed to transform '#{match}': #{e.message}")
|
|
126
|
+
match # Keep original if transformation fails
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
logger&.debug("Matched #{matches} timestamp(s)") if logger && matches > 0
|
|
132
|
+
|
|
133
|
+
result
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
#
|
|
137
|
+
# Validate that a matched string is actually a timestamp.
|
|
138
|
+
#
|
|
139
|
+
# @param [String] str
|
|
140
|
+
# The matched string
|
|
141
|
+
#
|
|
142
|
+
# @param [Regexp] pattern
|
|
143
|
+
# The pattern that matched
|
|
144
|
+
#
|
|
145
|
+
# @return [Boolean]
|
|
146
|
+
# true if valid timestamp
|
|
147
|
+
#
|
|
148
|
+
def valid_timestamp?(str, pattern)
|
|
149
|
+
return valid_unix?(str) if pattern == P08_UNIX_TIMESTAMP
|
|
150
|
+
true
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
#
|
|
154
|
+
# Validate unix timestamp is in reasonable range.
|
|
155
|
+
#
|
|
156
|
+
# @param [String] str
|
|
157
|
+
# The matched string
|
|
158
|
+
#
|
|
159
|
+
# @return [Boolean]
|
|
160
|
+
# true if valid unix timestamp (2001-2036)
|
|
161
|
+
#
|
|
162
|
+
def valid_unix?(str)
|
|
163
|
+
int = str.to_i
|
|
164
|
+
# Range: 2001-09-09 (first 10-digit) to 2036-07-11 (11 years ahead)
|
|
165
|
+
# This avoids false positives from phone numbers, order IDs, etc.
|
|
166
|
+
int >= 1_000_000_000 && int <= 2_100_000_000
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'timestamp'
|
|
4
|
+
|
|
5
|
+
module Zone
|
|
6
|
+
module Transform
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
#
|
|
10
|
+
# Build a transformation lambda.
|
|
11
|
+
#
|
|
12
|
+
# @param [String, Symbol] zone
|
|
13
|
+
# Zone name to convert to
|
|
14
|
+
#
|
|
15
|
+
# @param [Symbol, Hash] format
|
|
16
|
+
# Format specification (:to_iso8601, :to_unix, {pretty: 1}, {strftime: "..."})
|
|
17
|
+
#
|
|
18
|
+
# @return [Proc]
|
|
19
|
+
# Lambda that transforms a timestamp string
|
|
20
|
+
#
|
|
21
|
+
def build(zone:, format:)
|
|
22
|
+
->(value) do
|
|
23
|
+
timestamp = Timestamp.parse(value)
|
|
24
|
+
converted = convert_zone(timestamp, zone)
|
|
25
|
+
format_timestamp(converted, format)
|
|
26
|
+
rescue ArgumentError
|
|
27
|
+
nil
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
#
|
|
32
|
+
# Convert timestamp to specified zone.
|
|
33
|
+
#
|
|
34
|
+
# @param [Timestamp] timestamp
|
|
35
|
+
# @param [String, Symbol] zone_name
|
|
36
|
+
#
|
|
37
|
+
# @return [Timestamp]
|
|
38
|
+
#
|
|
39
|
+
def convert_zone(timestamp, zone_name)
|
|
40
|
+
case zone_name
|
|
41
|
+
in 'utc' | 'UTC'
|
|
42
|
+
timestamp.in_utc
|
|
43
|
+
in 'local'
|
|
44
|
+
timestamp.in_local
|
|
45
|
+
else
|
|
46
|
+
timestamp.in_zone(zone_name)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
#
|
|
51
|
+
# Format timestamp according to format specification.
|
|
52
|
+
#
|
|
53
|
+
# @param [Timestamp] timestamp
|
|
54
|
+
# @param [Symbol, Hash] format_spec
|
|
55
|
+
#
|
|
56
|
+
# @return [String, Integer]
|
|
57
|
+
#
|
|
58
|
+
def format_timestamp(timestamp, format_spec)
|
|
59
|
+
case format_spec
|
|
60
|
+
in :to_iso8601 | :to_unix
|
|
61
|
+
timestamp.send(format_spec)
|
|
62
|
+
in { pretty: Integer => style }
|
|
63
|
+
timestamp.to_pretty(style)
|
|
64
|
+
in { strftime: String => fmt }
|
|
65
|
+
timestamp.strftime(fmt)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
data/lib/zone/version.rb
CHANGED
data/lib/zone.rb
CHANGED
|
@@ -1,8 +1,52 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'tzinfo'
|
|
3
4
|
require_relative "zone/version"
|
|
5
|
+
require_relative "zone/field_mapping"
|
|
6
|
+
require_relative "zone/field_line"
|
|
7
|
+
require_relative "zone/timestamp"
|
|
8
|
+
require_relative "zone/cli"
|
|
4
9
|
|
|
5
10
|
module Zone
|
|
6
11
|
class Error < StandardError; end
|
|
7
|
-
|
|
12
|
+
|
|
13
|
+
def self.find(keyword)
|
|
14
|
+
TZInfo::Timezone.get(keyword)
|
|
15
|
+
rescue TZInfo::InvalidTimezoneIdentifier
|
|
16
|
+
search_fuzzy(keyword)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.search_fuzzy(keyword)
|
|
20
|
+
all_zones = TZInfo::Timezone.all_identifiers
|
|
21
|
+
|
|
22
|
+
# Normalize keyword: replace spaces with wildcards for matching
|
|
23
|
+
normalized_keyword = keyword.gsub(/\s+/, '.*')
|
|
24
|
+
|
|
25
|
+
# Try US wildcard pattern first
|
|
26
|
+
us_pattern = Regexp.new(
|
|
27
|
+
normalized_keyword.gsub(/^(?:US)?\/?/, 'US/').gsub(/$/,'.*'),
|
|
28
|
+
Regexp::IGNORECASE
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# Then try global pattern
|
|
32
|
+
global_pattern = Regexp.new(
|
|
33
|
+
".*#{normalized_keyword}.*",
|
|
34
|
+
Regexp::IGNORECASE
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
found = case all_zones
|
|
38
|
+
in [*, ^(us_pattern) => zone, *]
|
|
39
|
+
zone
|
|
40
|
+
in [*, ^(global_pattern) => zone, *]
|
|
41
|
+
zone
|
|
42
|
+
else
|
|
43
|
+
nil
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
return nil unless found
|
|
47
|
+
|
|
48
|
+
TZInfo::Timezone.get(found)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private_class_method :search_fuzzy
|
|
8
52
|
end
|
data/todo.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
## Rules
|
|
2
|
+
1. VERY IMPORTANT .Before using ANY Ruby class, method, builtin, expression, you are to read the latest docs on it. This can be found by executing `ri --no-pager <subject>`. For example:
|
|
3
|
+
|
|
4
|
+
```
|
|
5
|
+
ri --no-pager 'Array#zip'
|
|
6
|
+
```
|
|
7
|
+
|
|
8
|
+
Additionally, more comprehensive documentation can be found by accessing these documents, using the syntax:
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
ri --no-pager 'syntax/pattern_matching:'
|
|
12
|
+
```
|
|
13
|
+
which renders the document syntax/pattern_matching.rdoc. All other advanced documentation can be found below:
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
bsearch.rdoc
|
|
17
|
+
bug_triaging.rdoc
|
|
18
|
+
case_mapping.rdoc
|
|
19
|
+
character_selectors.rdoc
|
|
20
|
+
command_injection.rdoc
|
|
21
|
+
contributing.md
|
|
22
|
+
contributing/building_ruby.md
|
|
23
|
+
contributing/documentation_guide.md
|
|
24
|
+
contributing/glossary.md
|
|
25
|
+
contributing/making_changes_to_ruby.md
|
|
26
|
+
contributing/making_changes_to_stdlibs.md
|
|
27
|
+
contributing/reporting_issues.md
|
|
28
|
+
contributing/testing_ruby.md
|
|
29
|
+
date/calendars.rdoc
|
|
30
|
+
dig_methods.rdoc
|
|
31
|
+
distribution.md
|
|
32
|
+
dtrace_probes.rdoc
|
|
33
|
+
encodings.rdoc
|
|
34
|
+
exceptions.md
|
|
35
|
+
extension.ja.rdoc
|
|
36
|
+
extension.rdoc
|
|
37
|
+
fiber.md
|
|
38
|
+
format_specifications.rdoc
|
|
39
|
+
globals.rdoc
|
|
40
|
+
implicit_conversion.rdoc
|
|
41
|
+
index.md
|
|
42
|
+
maintainers.md
|
|
43
|
+
marshal.rdoc
|
|
44
|
+
memory_view.md
|
|
45
|
+
optparse/argument_converters.rdoc
|
|
46
|
+
optparse/creates_option.rdoc
|
|
47
|
+
optparse/option_params.rdoc
|
|
48
|
+
optparse/tutorial.rdoc
|
|
49
|
+
packed_data.rdoc
|
|
50
|
+
ractor.md
|
|
51
|
+
regexp/methods.rdoc
|
|
52
|
+
regexp/unicode_properties.rdoc
|
|
53
|
+
rjit/rjit.md
|
|
54
|
+
ruby/option_dump.md
|
|
55
|
+
ruby/options.md
|
|
56
|
+
security.rdoc
|
|
57
|
+
signals.rdoc
|
|
58
|
+
standard_library.md
|
|
59
|
+
strftime_formatting.rdoc
|
|
60
|
+
syntax.rdoc
|
|
61
|
+
syntax/assignment.rdoc
|
|
62
|
+
syntax/calling_methods.rdoc
|
|
63
|
+
syntax/comments.rdoc
|
|
64
|
+
syntax/control_expressions.rdoc
|
|
65
|
+
syntax/exceptions.rdoc
|
|
66
|
+
syntax/keywords.rdoc
|
|
67
|
+
syntax/literals.rdoc
|
|
68
|
+
syntax/methods.rdoc
|
|
69
|
+
syntax/miscellaneous.rdoc
|
|
70
|
+
syntax/modules_and_classes.rdoc
|
|
71
|
+
syntax/operators.rdoc
|
|
72
|
+
syntax/pattern_matching.rdoc
|
|
73
|
+
syntax/precedence.rdoc
|
|
74
|
+
syntax/refinements.rdoc
|
|
75
|
+
windows.md
|
|
76
|
+
yjit/yjit.md
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
You must do this for EVERY method, class, etc when you use it in this session for the first time
|
|
80
|
+
|
|
81
|
+
2. Prefer "in" to "when". See `ri --no-pager syntax/pattern_matching:`
|
|
82
|
+
|
|
83
|
+
3. When using OptionParser, prefer the implict, blockless declarations that utilize `OptionParser#parse!(into: options)`
|
|
84
|
+
|
|
85
|
+
4. Prefer a functional chaining style when doing consecutive operations on enumerables
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: zone
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- David Gillis
|
|
@@ -76,9 +76,27 @@ files:
|
|
|
76
76
|
- LICENSE.txt
|
|
77
77
|
- README.md
|
|
78
78
|
- Rakefile
|
|
79
|
+
- TEST_PLAN.md
|
|
80
|
+
- completions/README.md
|
|
81
|
+
- completions/_zone
|
|
82
|
+
- docs/user-experience-review.md
|
|
79
83
|
- exe/zone
|
|
80
84
|
- lib/zone.rb
|
|
85
|
+
- lib/zone/cli.rb
|
|
86
|
+
- lib/zone/colors.rb
|
|
87
|
+
- lib/zone/field.rb
|
|
88
|
+
- lib/zone/field_line.rb
|
|
89
|
+
- lib/zone/field_mapping.rb
|
|
90
|
+
- lib/zone/input.rb
|
|
91
|
+
- lib/zone/logging.rb
|
|
92
|
+
- lib/zone/options.rb
|
|
93
|
+
- lib/zone/output.rb
|
|
94
|
+
- lib/zone/pattern.rb
|
|
95
|
+
- lib/zone/timestamp.rb
|
|
96
|
+
- lib/zone/timestamp_patterns.rb
|
|
97
|
+
- lib/zone/transform.rb
|
|
81
98
|
- lib/zone/version.rb
|
|
99
|
+
- todo.md
|
|
82
100
|
homepage: https://github.com/gillisd/zone
|
|
83
101
|
licenses:
|
|
84
102
|
- MIT
|