Module: Tuile::Locale::DateFormats
- Defined in:
- lib/tuile/locale.rb,
sig/tuile.rbs
Overview
The two rules a strftime date format list obeys: what may be in one (DateFormats.validate) and what one looks like to a human (DateFormats.humanize). Both answer at assignment, so a bad format raises there rather than at the first keystroke. TimeFormats is its sibling over the same Formats lexer.
Constant Summary collapse
- REF =
The date every format is round-tripped against. Every property is load-bearing: pre-1969 so
%yfails (it cannot carry a century), post-1582-10-15 so the Gregorian reform fails no innocent format, and month ≠ day so a%m/%dswap is not masked. A canary rather than a proof — but a century-lossy directive is lossy in both directions, so one pre-window date catches the class that ships. Date.new(1962, 9, 4)
- HINTS =
The directives humanize can turn into a placeholder. There is deliberately no
%b/%B: a month name would need an inventedmmm, and an app typing month names sets its own hint. { "%Y" => "yyyy", "%m" => "mm", "%d" => "dd", "%%" => "%" }.freeze
Class Method Summary collapse
-
.humanize(format) ⇒ String?
Translates a format into a typing hint, or
nilwhen it holds any directive HINTS does not cover. - .malformed ⇒ String
-
.parses?(format) ⇒ Boolean
@param
format. -
.rejection(format, primary: true) ⇒ String
@param
format. -
.round_trips?(format) ⇒ Boolean
@param
format. -
.validate(list) ⇒ ::Array[String]
Normalizes one format or a list of them into a frozen
Arrayof frozenStrings, validating each. -
.validate_one(format, primary: true) ⇒ String
@param
format. -
.widen(format) ⇒ String
Rewrites every
%yinformatas%Y, leaving the rest alone.
Class Method Details
.humanize(format) ⇒ String?
Translates a format into a typing hint, or nil when it holds any
directive HINTS does not cover.
DateFormats.humanize("%d.%m.%Y") # => "dd.mm.yyyy"
DateFormats.humanize("%Y-%j") # => nil, rather than "yyyy-%j"
@param format
@return — frozen.
199 |
# File 'lib/tuile/locale.rb', line 199 def humanize(format) = Formats.humanize(format, HINTS) |
.malformed ⇒ String
278 279 280 281 |
# File 'lib/tuile/locale.rb', line 278 def malformed "it is incomplete, is write-only (strptime takes no `-` flag), " \ "or is not the directive you meant" end |
.parses?(format) ⇒ Boolean
@param format
@return — true iff strptime consumes its own strftime
output whole. Weaker than round_trips? on purpose: "%d/%m/%y"
parses fine, it just parses to the wrong century.
253 254 255 256 257 258 |
# File 'lib/tuile/locale.rb', line 253 def parses?(format) parsed = Date._strptime(REF.strftime(format), format) !parsed.nil? && parsed[:leftover].to_s.empty? rescue ArgumentError false end |
.rejection(format, primary: true) ⇒ String
@param format
@param primary
@return — why the check failed, in the terms most likely to be the caller's actual mistake.
264 265 266 267 268 269 270 271 272 273 274 275 |
# File 'lib/tuile/locale.rb', line 264 def rejection(format, primary: true) return "#{format.inspect} is not a usable strptime pattern: #{malformed}" unless primary reason = if format.include?("%y") "%y cannot carry a century (Ruby reads 69 as 1969 and 26 as 2026), so write %Y — " \ "it may still appear later in the list, where it only ever parses" else malformed end "#{format.inspect} does not survive a strftime/strptime round-trip: #{reason}" end |
.round_trips?(format) ⇒ Boolean
243 244 245 246 247 |
# File 'lib/tuile/locale.rb', line 243 def round_trips?(format) Date.strptime(REF.strftime(format), format) == REF rescue ArgumentError # Date::Error is one; so is an unparseable format false end |
.validate(list) ⇒ ::Array[String]
Normalizes one format or a list of them into a frozen Array of frozen
Strings, validating each.
DateFormats.validate("%d.%m.%Y") # => ["%d.%m.%Y"]
The primary is held to a stricter rule than the rest. formats.first
is what a field writes, so it must survive a strftime/strptime
round-trip; every later entry only ever parses, so it need only be a
usable strptime pattern — which is how a lenient list carries a
two-digit-year pattern behind its widened one.
@param list
@return — frozen, as are its elements.
183 184 185 186 187 188 189 |
# File 'lib/tuile/locale.rb', line 183 def validate(list) formats = list.instance_of?(String) ? [list] : list raise TypeError, "expected a String or an Array of Strings, got #{list.inspect}" unless formats.is_a?(Array) raise ArgumentError, "expected at least one format" if formats.empty? formats.each_with_index.map { |format, index| validate_one(format, primary: index.zero?) }.freeze end |
.validate_one(format, primary: true) ⇒ String
@param format
@param primary — whether this is formats.first, which is written as well as read and so must round-trip.
@return — a frozen copy.
228 229 230 231 232 233 234 235 236 237 238 |
# File 'lib/tuile/locale.rb', line 228 def validate_one(format, primary: true) raise TypeError, "expected a String format, got #{format.inspect}" unless format.instance_of?(String) lookalike = Formats.lookalike(format) raise ArgumentError, "#{lookalike} is not locale-aware in Ruby (it is a fixed American format)" if lookalike usable = primary ? round_trips?(format) : parses?(format) raise ArgumentError, rejection(format, primary: primary) unless usable format.dup.freeze end |
.widen(format) ⇒ String
Rewrites every %y in format as %Y, leaving the rest alone.
DateFormats.widen("%d/%m/%y") # => "%d/%m/%Y"
DateFormats.widen("100%%y") # => "100%%y" — that is a literal %
For validate's benefit: a two-digit year cannot round-trip, since
Date.new(1962, 9, 4) renders "04/09/62" and reparses as 2062
under Ruby's fixed POSIX window. So Tuile::Locale.system widens a detected
d_fmt here rather than losing it, where an app assigning the same
pattern gets the rejection instead (D_locale).
@param format
@return — frozen.
214 215 216 217 218 219 220 |
# File 'lib/tuile/locale.rb', line 214 def widen(format) widened = +"" Formats.each_directive(format) do |directive| widened << (directive.end_with?("y") && directive.length > 1 ? "#{directive[0..-2]}Y" : directive) end widened.freeze end |