Module: NA::Types
- Defined in:
- lib/na/types.rb
Overview
Custom types for GLI Provides natural language date/time and duration parsing Uses chronify gem for parsing
Class Method Summary collapse
-
.normalize_relative_duration(value, default_past: false) ⇒ String
Normalize shorthand relative durations to phrases Chronic can parse.
-
.parse_date_begin(value) ⇒ Time
Parse a natural-language/iso date string for a start time.
-
.parse_date_end(value) ⇒ Time
Parse a natural-language/iso date string for an end time.
-
.parse_duration_seconds(value) ⇒ Integer
Convert duration expressions to seconds Supports: “90” (minutes), “45m”, “2h”, “1d2h30m”, with optional leading ‘-’ or trailing ‘ago’ Also supports “2:30”, “2:30 ago”, and word forms like “2 hours 30 minutes (ago)”.
Class Method Details
.normalize_relative_duration(value, default_past: false) ⇒ String
Normalize shorthand relative durations to phrases Chronic can parse. Examples:
- "30m ago" => "30 minutes ago"
- "-30m" => "30 minutes ago"
- "2h30m" => "2 hours 30 minutes ago" (when default_past)
- "2h 30m ago" => "2 hours 30 minutes ago"
- "2:30 ago" => "2 hours 30 minutes ago"
- "-2:30" => "2 hours 30 minutes ago"
Accepts d,h,m units; hours:minutes pattern; optional leading ‘-’; optional ‘ago’.
24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
# File 'lib/na/types.rb', line 24 def normalize_relative_duration(value, default_past: false) return value if value.nil? s = value.to_s.strip return s if s.empty? has_ago = s =~ /\bago\b/i negative = s.start_with?('-') text = s.sub(/^[-+]/, '') # hours:minutes pattern (e.g., 2:30, 02:30) if (m = text.match(/^(\d{1,2}):(\d{1,2})(?:\s*ago)?$/i)) hours = m[1].to_i minutes = m[2].to_i parts = [] parts << "#{hours} hours" if hours.positive? parts << "#{minutes} minutes" if minutes.positive? return "#{parts.join(' ')} ago" end # Compound d/h/m (order independent, allow spaces): e.g., 1d2h30m, 2h 30m, 30m days = hours = minutes = 0 found = false if (dm = text.match(/(?:(\d+)\s*d)/i)) days = dm[1].to_i found = true end if (hm = text.match(/(?:(\d+)\s*h)/i)) hours = hm[1].to_i found = true end if (mm = text.match(/(?:(\d+)\s*m)/i)) minutes = mm[1].to_i found = true end if found parts = [] parts << "#{days} days" if days.positive? parts << "#{hours} hours" if hours.positive? parts << "#{minutes} minutes" if minutes.positive? # Determine if we should make it past-tense return "#{parts.join(' ')} ago" if negative || has_ago || default_past return parts.join(' ') end # Fall through: not a shorthand we handle s end |
.parse_date_begin(value) ⇒ Time
Parse a natural-language/iso date string for a start time
80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 |
# File 'lib/na/types.rb', line 80 def parse_date_begin(value) return nil if value.nil? || value.to_s.strip.empty? # Prefer explicit ISO first (only if the value looks ISO-like) iso_rx = /\A\d{4}-\d{2}-\d{2}(?:[ T]\d{1,2}:\d{2}(?::\d{2})?)?\z/ if value.to_s.strip =~ iso_rx begin return Time.parse(value) rescue StandardError # fall through to chronify end end # Fallback to chronify with guess begin begin # Normalize shorthand (e.g., 2h30m, -2:30, 30m ago) txt = normalize_relative_duration(value.to_s, default_past: true) # Bias to past for expressions like "ago", "yesterday", or "last ..." future = txt !~ /(\bago\b|yesterday|\blast\b)/i result = txt.chronify(guess: :begin, future: future) NA.notify("Parsed '#{value}' as #{result}", debug: true) if result result rescue StandardError nil end end |
.parse_date_end(value) ⇒ Time
Parse a natural-language/iso date string for an end time
110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 |
# File 'lib/na/types.rb', line 110 def parse_date_end(value) return nil if value.nil? || value.to_s.strip.empty? # Prefer explicit ISO first (only if the value looks ISO-like) iso_rx = /\A\d{4}-\d{2}-\d{2}(?:[ T]\d{1,2}:\d{2}(?::\d{2})?)?\z/ if value.to_s.strip =~ iso_rx begin return Time.parse(value) rescue StandardError # fall through to chronify end end # Fallback to chronify with guess end value.to_s.chronify(guess: :end, future: false) end |
.parse_duration_seconds(value) ⇒ Integer
Convert duration expressions to seconds Supports: “90” (minutes), “45m”, “2h”, “1d2h30m”, with optional leading ‘-’ or trailing ‘ago’ Also supports “2:30”, “2:30 ago”, and word forms like “2 hours 30 minutes (ago)”
132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 |
# File 'lib/na/types.rb', line 132 def parse_duration_seconds(value) return nil if value.nil? s = value.to_s.strip return nil if s.empty? # Strip leading sign and optional 'ago' s = s.sub(/^[-+]/, '') s = s.sub(/\bago\b/i, '').strip # H:MM pattern m = s.match(/^(\d{1,2}):(\d{1,2})$/) if m hours = m[1].to_i minutes = m[2].to_i return (hours * 3600) + (minutes * 60) end # d/h/m compact with letters, order independent (e.g., 1d2h30m, 2h 30m, 30m) m = s.match(/^(?:(?<day>\d+)\s*d)?\s*(?:(?<hour>\d+)\s*h)?\s*(?:(?<min>\d+)\s*m)?$/i) if m && !m[0].strip.empty? && (m['day'] || m['hour'] || m['min']) return [[m['day'], 86_400], [m['hour'], 3600], [m['min'], 60]].map { |q, mult| q ? q.to_i * mult : 0 }.sum end # Word forms: e.g., "2 hours 30 minutes", "1 day 2 hours", etc. days = 0 hours = 0 minutes = 0 found_word = false if (dm = s.match(/(\d+)\s*(?:day|days)\b/i)) days = dm[1].to_i found_word = true end if (hm = s.match(/(\d+)\s*(?:hour|hours|hr|hrs)\b/i)) hours = hm[1].to_i found_word = true end if (mm = s.match(/(\d+)\s*(?:minute|minutes|min|mins)\b/i)) minutes = mm[1].to_i found_word = true end return (days * 86_400) + (hours * 3600) + (minutes * 60) if found_word # Plain number => minutes return s.to_i * 60 if s =~ /^\d+$/ # Last resort: try chronify two points and take delta begin start = Time.now finish = s.chronify(context: 'now', guess: :end, future: false) return (finish - start).abs.to_i if finish rescue StandardError # ignore end nil end |