fbtxt-lexer 0.0.1

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.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +3 -0
  3. data/Manifest.txt +47 -0
  4. data/README.md +18 -0
  5. data/Rakefile +30 -0
  6. data/lib/fbtxt/lexer/debuggable.rb +58 -0
  7. data/lib/fbtxt/lexer/lexer-logger.rb +20 -0
  8. data/lib/fbtxt/lexer/lexer-on_goal.rb +167 -0
  9. data/lib/fbtxt/lexer/lexer-on_group_def.rb +31 -0
  10. data/lib/fbtxt/lexer/lexer-on_prop_cards.rb +61 -0
  11. data/lib/fbtxt/lexer/lexer-on_prop_lineup.rb +82 -0
  12. data/lib/fbtxt/lexer/lexer-on_prop_misc.rb +108 -0
  13. data/lib/fbtxt/lexer/lexer-on_prop_penalties.rb +44 -0
  14. data/lib/fbtxt/lexer/lexer-on_round_def.rb +37 -0
  15. data/lib/fbtxt/lexer/lexer-on_top.rb +133 -0
  16. data/lib/fbtxt/lexer/lexer-prep_doc.rb +131 -0
  17. data/lib/fbtxt/lexer/lexer-prep_line.rb +63 -0
  18. data/lib/fbtxt/lexer/lexer-props.rb +66 -0
  19. data/lib/fbtxt/lexer/lexer-tokenize_line.rb +381 -0
  20. data/lib/fbtxt/lexer/lexer-tokenize_norm.rb +93 -0
  21. data/lib/fbtxt/lexer/lexer.rb +192 -0
  22. data/lib/fbtxt/lexer/lexer_buffer.rb +68 -0
  23. data/lib/fbtxt/lexer/lexer_context.rb +70 -0
  24. data/lib/fbtxt/lexer/lexer_token.rb +127 -0
  25. data/lib/fbtxt/lexer/token-date--helpers.rb +130 -0
  26. data/lib/fbtxt/lexer/token-date--names.rb +108 -0
  27. data/lib/fbtxt/lexer/token-date.rb +200 -0
  28. data/lib/fbtxt/lexer/token-date_duration.rb +171 -0
  29. data/lib/fbtxt/lexer/token-geo.rb +173 -0
  30. data/lib/fbtxt/lexer/token-goals--helpers.rb +114 -0
  31. data/lib/fbtxt/lexer/token-goals.rb +306 -0
  32. data/lib/fbtxt/lexer/token-group.rb +29 -0
  33. data/lib/fbtxt/lexer/token-note.rb +40 -0
  34. data/lib/fbtxt/lexer/token-prop.rb +334 -0
  35. data/lib/fbtxt/lexer/token-prop_name.rb +83 -0
  36. data/lib/fbtxt/lexer/token-round.rb +88 -0
  37. data/lib/fbtxt/lexer/token-score--helpers.rb +189 -0
  38. data/lib/fbtxt/lexer/token-score.rb +60 -0
  39. data/lib/fbtxt/lexer/token-score_full.rb +331 -0
  40. data/lib/fbtxt/lexer/token-score_fuller.rb +434 -0
  41. data/lib/fbtxt/lexer/token-score_legs.rb +59 -0
  42. data/lib/fbtxt/lexer/token-status.rb +192 -0
  43. data/lib/fbtxt/lexer/token-status_inline.rb +112 -0
  44. data/lib/fbtxt/lexer/token-text.rb +221 -0
  45. data/lib/fbtxt/lexer/token-time.rb +144 -0
  46. data/lib/fbtxt/lexer/token.rb +224 -0
  47. data/lib/fbtxt/lexer/version.rb +24 -0
  48. data/lib/fbtxt/lexer.rb +118 -0
  49. metadata +142 -0
@@ -0,0 +1,68 @@
1
+
2
+ module Fbtxt
3
+
4
+ ## note - Tokens was placed inside Lexer - keep "top-level" for now inside Fbtxt
5
+ ## for easier reuse with (new) lexer variants!!
6
+
7
+ ## transforms
8
+ ##
9
+ ## Netherlands 1-2 (1-1) England
10
+ ## => text => team
11
+ ## score|vs
12
+ ## text => team
13
+
14
+
15
+
16
+ ## token iter/find better name
17
+ ## e.g. TokenBuffer/Scanner or such ??
18
+ class Tokens
19
+ def initialize( tokens )
20
+ @tokens = tokens
21
+ @pos = 0
22
+ end
23
+
24
+ def pos() @pos; end
25
+ def eos?() @pos >= @tokens.size; end
26
+
27
+
28
+
29
+
30
+ ## pattern e.g. [:TEXT, [:VS,:SCORE], :TEXT]
31
+ def match?( *pattern )
32
+ ## puts " starting match? #{pattern.inspect} @ #{@pos}"
33
+ pattern.each_with_index do |types,offset|
34
+ tok = peek(offset)
35
+ return false if tok.nil? ## no more tokens (cannot match)
36
+
37
+ ## if single symbol wrap in array
38
+ types = types.is_a?(Array) ? types : [types]
39
+ return false unless types.include?( tok.type )
40
+ end
41
+ true
42
+ end
43
+
44
+
45
+
46
+ def cur() peek(0); end
47
+
48
+ def peek(offset=1)
49
+ ## return nil if eos
50
+ if @pos+offset >= @tokens.size
51
+ nil
52
+ else
53
+ @tokens[@pos+offset]
54
+ end
55
+ end
56
+
57
+ def next
58
+ # if @pos >= @tokens.size
59
+ # raise ArgumentError, "end of array - #{@pos} >= #{@tokens.size}"
60
+ # end
61
+ # throw (standard) end of iteration here why? why not?
62
+
63
+ t = @tokens[@pos]
64
+ @pos += 1
65
+ t
66
+ end
67
+ end # class Tokens
68
+ end # module Fbtxt
@@ -0,0 +1,70 @@
1
+ module Fbtxt
2
+ class Lexer
3
+
4
+
5
+ ###
6
+ ## use nested class for context - why? why not?
7
+ ## note: first arg passed in MUST be ref to lexer (instance)
8
+ class Context
9
+ ## passed along to on_round_def etc. handlers in tokenize_line
10
+ ## note - for now only offset (in line begin/end) gets updated !!!
11
+ attr_writer :offset
12
+ attr_reader :lineno
13
+
14
+ def initialize( lexer,
15
+ line:,
16
+ lineno:,
17
+ errors: )
18
+ @lexer = lexer
19
+ @line = line
20
+ @lineno = lineno
21
+ @errors = errors
22
+
23
+ @offset = [0,0] ## or use [] aka [nil,nil] for not defined??? why? why not?
24
+ ## @offset = offset ## MatchData offset e.g. [m.begin(0),m.end(0)]
25
+ end
26
+
27
+
28
+
29
+ def warn_on_else( match, mode: 'TOP' )
30
+ if match[:any]
31
+ _add_warn( "unexpected char >#{match[:any]}< (#{mode})" )
32
+ else
33
+ ## internal error - shouldn't really happen
34
+ _add_warn( "internal error - unknown match (#{mode}): #{match.inspect}")
35
+ end
36
+ end
37
+
38
+
39
+ def _add_warn( msg )
40
+ ## note - warns gets logged as error for now too
41
+ ## maybe add @warns later - why? why not?
42
+ ##
43
+ ## note - add +1 to offset (start at one - not zero-based)
44
+ ## will match with (external) text editors
45
+ msg = "parse error (tokenize) - " +
46
+ msg +
47
+ " in line @#{@lineno}:#{@offset[0]+1},#{@offset[1]+1} >#{@line}< "
48
+
49
+ @errors << msg
50
+ @lexer.log( "!! WARN - #{msg}" )
51
+
52
+ @lexer._warn( msg )
53
+ end
54
+
55
+ =begin
56
+ ## use report/log/??_parses_error
57
+ def _add_error( msg )
58
+ msg = "parse error (tokenize) -" +
59
+ msg +
60
+ " in line #{@lineno}@#{@offset[0]},#{@offse[1]} >#{@line}< "
61
+
62
+ @errors << msg
63
+ end
64
+ =end
65
+
66
+ end # class Context
67
+
68
+
69
+ end ## class Lexer
70
+ end ## module Fbtxt
@@ -0,0 +1,127 @@
1
+ module Fbtxt
2
+ class Lexer
3
+
4
+
5
+
6
+ class Token
7
+
8
+ ## Token.newline( lineno: 1, offset: [1,2] )
9
+ ## maps to =>
10
+ ## Token.new( :NEWLINE, "\n", lineno: 1, offset: [1,2])
11
+ ##
12
+ ## use self.nl ?
13
+ def self.newline( lineno:, offset: [])
14
+ new( :NEWLINE, "\n", lineno: lineno, offset: offset )
15
+ end
16
+
17
+ ## Token.literal( ",", lineno: 4, offset: [5,6])
18
+ ## # maps to =>
19
+ ## Token.new( ",", ",", lineno: 4, offset: [5,6])
20
+ ##
21
+ ## use self.lit?
22
+ def self.literal( literal, lineno:, offset: [])
23
+ new( literal, literal, lineno: lineno, offset: offset )
24
+ end
25
+
26
+ ## or use virt or pseudo - why? why not?
27
+ def self.virtual( type, lineno: -1, offset: [])
28
+ ## note - offset (start/end) should be same number (zero-width assertions!!)
29
+ ## e.g. :GOALS_COMPAT, "<|GOALS_COMPAT|>"
30
+ new( type, '', lineno: lineno, offset: offset )
31
+ end
32
+
33
+
34
+
35
+ attr_reader :type, :text,
36
+ :lineno, :offset
37
+
38
+ def initialize( type, text='',
39
+ lineno:, offset: [],
40
+ value: nil )
41
+ @type = type
42
+ @text = text # note - lexeme (string from source)
43
+ @lineno = lineno # note - lineno (integer number - not line as string) !!!
44
+
45
+ raise TypeError, "type Array required for offset; got #{offset.inspect}" unless offset.is_a?( Array )
46
+ @offset = offset # note - for now char offset [start,end] in line (NOT absolute!!)
47
+ # maybe latter add MatchData#byteoffset instead - why? why not?
48
+ @value = value # might be (union of) string/array/hash
49
+ end
50
+
51
+ def value
52
+ ## note - if value is not set (nil) return text (lexeme)
53
+ ## no need to duplicate text as value
54
+ @value.nil? ? @text : @value
55
+ end
56
+
57
+
58
+ ## note: do NOT use as_text/text to avoid confusion with (raw) text (lexeme)
59
+ ##
60
+ ## use
61
+ ## as_str -- value (as String)
62
+ ## as_int -- value (as Integer)
63
+ ## as_hash -- value (as Hash)
64
+ ## as_ary -- value (as Array)
65
+
66
+ def as_str
67
+ raise TypeError, "token value #{@value.inspect} is #{@value.class.name} NOT string; sorry" if @value && !@value.is_a?(String)
68
+ ## note - if value is not set (nil) return text (lexeme)
69
+ ## no need to duplicate text as value
70
+ @value.nil? ? @text : @value
71
+ end
72
+
73
+ def as_int
74
+ raise TypeError, "token value #{@value.inspect} is #{@value.class.name} NOT integer; sorry" if !@value.is_a?(Integer)
75
+ @value
76
+ end
77
+
78
+ def as_hash
79
+ raise TypeError, "token value #{@value.inspect} is #{@value.class.name} NOT hash; sorry" if !@value.is_a?(Hash)
80
+ @value
81
+ end
82
+
83
+ def as_ary
84
+ raise TypeError, "token value #{@value.inspect} is #{@value.class.name} NOT array; sorry" if !@value.is_a?(Array)
85
+ @value
86
+ end
87
+
88
+
89
+ def to_legacy
90
+ ## return old "legacy" array format
91
+ if @value.nil?
92
+ [@type, @text]
93
+ else
94
+ [@type, [@text, @value]]
95
+ end
96
+ end
97
+
98
+
99
+ ## pretty print
100
+ def pretty_print( printer )
101
+ ## check for literal e.g. "," etc.
102
+ if @type.is_a?( String ) && @type == @text && @value.nil?
103
+ printer.text( "[#{@type.inspect}" )
104
+ elsif @type.is_a?( Symbol ) && @text == '' && @value.nil?
105
+ ## assume virtual token (zero-width)
106
+ ## use <!...!> style
107
+ printer.text( "[<|#{@type}|>" )
108
+ else
109
+ printer.text( "[#{@type.inspect} #{@text.inspect}" )
110
+ printer.text( ", #{value.inspect}") if @value
111
+ end
112
+
113
+
114
+ printer.text( " @#{@lineno}" )
115
+ ## note - for now print only start_offset (offset[0])
116
+ ## to keep dump/output shorter
117
+ ## note - start counting columns at one (NOT zero), thus, add +1 !!
118
+ printer.text( ":#{@offset[0]+1}" ) if @offset.is_a?(Array) && @offset.size == 2
119
+ printer.text( "]" )
120
+ end
121
+
122
+ end # class Token
123
+
124
+
125
+
126
+ end # class Lexer
127
+ end # module Fbtxt
@@ -0,0 +1,130 @@
1
+ module Fbtxt
2
+ class Lexer
3
+
4
+
5
+ ## "internal" date helpers
6
+ def self._build_date( m )
7
+ date = {}
8
+ ## map month names
9
+ ## note - allow any/upcase JULY/JUL etc. thus ALWAYS downcase for lookup
10
+ date[:y] = m[:year].to_i(10) if m[:year]
11
+ ## check - use y too for two-digit year or keep separate - why? why not?
12
+ date[:yy] = m[:yy].to_i(10) if m[:yy] ## two digit year (e.g. 25 or 78 etc.)
13
+ date[:m] = m[:month].to_i(10) if m[:month]
14
+ date[:m] = MONTH_MAP[ m[:month_name].downcase ] if m[:month_name]
15
+ date[:d] = m[:day].to_i(10) if m[:day]
16
+ date[:wday] = DAY_MAP[ m[:day_name].downcase ] if m[:day_name]
17
+
18
+ date
19
+ end
20
+
21
+ def self._build_date_legs( m )
22
+ legs = {}
23
+ ## map month names
24
+ ## note - allow any/upcase JULY/JUL etc. thus ALWAYS downcase for lookup
25
+ date = {}
26
+ date[:m] = MONTH_MAP[ m[:month_name1].downcase ]
27
+ date[:d] = m[:day1].to_i(10)
28
+ legs[:date1] = date
29
+
30
+ date = {}
31
+ date[:m] = MONTH_MAP[ m[:month_name2].downcase ] if m[:month_name2]
32
+ date[:d] = m[:day2].to_i(10)
33
+ legs[:date2] = date
34
+
35
+ legs
36
+ end
37
+
38
+
39
+ def self._build_duration( m )
40
+ ## todo/check/fix - if end: works for kwargs!!!!!
41
+ duration = { start: {}, end: {}}
42
+
43
+ duration[:start][:y] = m[:year1].to_i(10) if m[:year1]
44
+ duration[:start][:m] = MONTH_MAP[ m[:month_name1].downcase ] if m[:month_name1]
45
+ duration[:start][:d] = m[:day1].to_i(10) if m[:day1]
46
+ duration[:start][:wday] = DAY_MAP[ m[:day_name1].downcase ] if m[:day_name1]
47
+
48
+ duration[:end][:y] = m[:year2].to_i(10) if m[:year2]
49
+ duration[:end][:m] = MONTH_MAP[ m[:month_name2].downcase ] if m[:month_name2]
50
+ duration[:end][:d] = m[:day2].to_i(10) if m[:day2]
51
+ duration[:end][:wday] = DAY_MAP[ m[:day_name2].downcase ] if m[:day_name2]
52
+
53
+ duration
54
+ end
55
+
56
+
57
+
58
+
59
+ def _build_date( m ) self.class._build_date( m ); end
60
+ def _build_date_legs( m ) self.class._build_date_legs( m ); end
61
+ def _build_duration( m ) self.class._build_duration( m ); end
62
+
63
+
64
+
65
+
66
+ #############
67
+ ## "top-level" add a date parser helper
68
+
69
+ ## note: parse_date - returns Date object
70
+ ## _parse_date (with underscore) - return hash of "parsed" regex match data!!
71
+
72
+ def self.parse_date( str, start: nil )
73
+ if m = _parse_date( str )
74
+ year = m[:y]
75
+ yy = m[:yy]
76
+
77
+ ####
78
+ ## support two digit shortcut for year
79
+ if yy && year.nil?
80
+ ###
81
+ ## for now assume 00,01 to 30 is 2000,2001 to 2030
82
+ ## and 31 to 99 is 1931 to 1999
83
+ year = yy <= 30 ? 2000+yy : 1900+yy
84
+ end
85
+
86
+ month = m[:m]
87
+ day = m[:d]
88
+ wday = m[:wday]
89
+
90
+
91
+ if year.nil? ## try to calculate year
92
+ raise ArgumentError, "year required in date >#{str}< or pass along start date" if start.nil?
93
+
94
+ year = if month > start.month ||
95
+ (month == start.month && day >= start.day)
96
+ # assume same year as start_at event (e.g. 2013 for 2013/14 season)
97
+ start.year
98
+ else
99
+ # assume year+1 as start_at event (e.g. 2014 for 2013/14 season)
100
+ start.year+1
101
+ end
102
+ end
103
+ Date.new( year,month,day )
104
+ else
105
+ raise ArgumentError, "unexpected date format; cannot parse >#{str}<"
106
+ end
107
+ end
108
+
109
+
110
+
111
+ def self._parse_date( str )
112
+ ## note - strip - leading/trailing spaces automatic - why? why not?
113
+ m = DATE_RE.match( str.strip )
114
+
115
+ if m && m.pre_match == '' && m.post_match == ''
116
+ ## return hash table with captured components
117
+ date = _build_date( m )
118
+ date
119
+ elsif m
120
+ ## note - match BUT not anchored to start and end-of-string!!!
121
+ ## report, error somehow??
122
+ nil
123
+ else
124
+ nil ## no match - return nil
125
+ end
126
+ end
127
+
128
+
129
+ end # class Lexer
130
+ end # module Fbtxt
@@ -0,0 +1,108 @@
1
+ module Fbtxt
2
+ class Lexer
3
+
4
+
5
+ def self.parse_names( txt )
6
+ lines = [] # array of lines (with words)
7
+
8
+ txt.each_line do |line|
9
+ line = line.strip
10
+
11
+ next if line.empty?
12
+ next if line.start_with?( '#' ) ## skip comments too
13
+
14
+ ## strip inline (until end-of-line) comments too
15
+ ## e.g. Janvier Janv Jan ## check janv in use??
16
+ ## => Janvier Janv Jan
17
+
18
+ line = line.sub( /#.*/, '' ).strip
19
+ ## pp line
20
+
21
+ values = line.split( /[ \t]+/ )
22
+ ## pp values
23
+
24
+ ## todo/fix -- add check for duplicates
25
+ lines << values
26
+ end
27
+ lines
28
+
29
+ end # method parse
30
+
31
+
32
+ def self.build_names( lines )
33
+ ## join all words together into a single string e.g.
34
+ ## January|Jan|February|Feb|March|Mar|April|Apr|May|June|Jun|...
35
+ lines.map { |line| line.join('|') }.join('|')
36
+ end
37
+
38
+
39
+ def self.build_map( lines, downcase: false )
40
+ ## note: downcase name!!!
41
+ ## build a lookup map that maps the word to the index (line no) plus 1 e.g.
42
+ ## {"january" => 1, "jan" => 1,
43
+ ## "february" => 2, "feb" => 2,
44
+ ## "march" => 3, "mar" => 3,
45
+ ## "april" => 4, "apr" => 4,
46
+ ## "may" => 5,
47
+ ## "june" => 6, "jun" => 6, ...
48
+ lines.each_with_index.reduce( {} ) do |h,(line,i)|
49
+ line.each do |name|
50
+ h[ downcase ? name.downcase : name ] = i+1
51
+ end ## note: start mapping with 1 (and NOT zero-based, that is, 0)
52
+ h
53
+ end
54
+ end
55
+
56
+
57
+
58
+
59
+ MONTH_LINES = parse_names( <<TXT )
60
+ January Jan
61
+ February Feb
62
+ March Mar
63
+ April Apr
64
+ May
65
+ June Jun
66
+ July Jul
67
+ August Aug
68
+ September Sept Sep
69
+ October Oct
70
+ November Nov
71
+ December Dec
72
+ TXT
73
+
74
+ MONTH_NAMES = build_names( MONTH_LINES )
75
+ # pp MONTH_NAMES
76
+ MONTH_MAP = build_map( MONTH_LINES, downcase: true )
77
+ # pp MONTH_MAP
78
+
79
+
80
+
81
+ DAY_LINES = parse_names( <<TXT )
82
+ Monday Mon Mo
83
+ Tuesday Tues Tue Tu
84
+ Wednesday Wed We
85
+ Thursday Thurs Thur Thu Th
86
+ Friday Fri Fr
87
+ Saturday Sat Sa
88
+ Sunday Sun Su
89
+ TXT
90
+
91
+ DAY_NAMES = build_names( DAY_LINES )
92
+ # pp DAY_NAMES
93
+ DAY_MAP = build_map( DAY_LINES, downcase: true )
94
+ # pp DAY_MAP
95
+
96
+
97
+ #=>
98
+ # "January|Jan|February|Feb|March|Mar|April|Apr|May|June|Jun|
99
+ # July|Jul|August|Aug|September|Sept|Sep|October|Oct|
100
+ # November|Nov|December|Dec"
101
+ #
102
+ # "Monday|Mon|Mo|Tuesday|Tues|Tue|Tu|Wednesday|Wed|We|
103
+ # Thursday|Thurs|Thur|Thu|Th|Friday|Fri|Fr|
104
+ # Saturday|Sat|Sa|Sunday|Sun|Su"
105
+
106
+
107
+ end # class Lexer
108
+ end # module Fbtxt
@@ -0,0 +1,200 @@
1
+ module Fbtxt
2
+ class Lexer
3
+
4
+
5
+
6
+ # e.g. Fri Aug 9
7
+ # Fri Aug 9
8
+ ## Fri, Aug 9
9
+ ## Fri, Aug 9 2024
10
+ ## Fri, Aug 9, 2024
11
+ ## Aug 9, 2024
12
+ ## Aug 9, 2024
13
+ ## note - eat-up optional comma after DAY_NAMES!!
14
+ ##
15
+ ## note - Fri Aug/9 no longer supported!!!
16
+ DATE_I_RE = %r{
17
+ (?<date>
18
+ \b
19
+ ## optional day name
20
+ ((?<day_name>#{DAY_NAMES})
21
+ (?: ,?[ ]+)
22
+ )?
23
+ (?<month_name>#{MONTH_NAMES})
24
+ [ ]
25
+ (?<day>\d{1,2})
26
+ \b
27
+ ## optional year
28
+ ( ,? [ ] ## note - comma optinal with single space required for now
29
+ (?<year>\d{4}) ## optional year 2025 (yyyy)
30
+ \b
31
+ )?
32
+ )}ix
33
+
34
+
35
+ ### todo/fix - add (opt) day_name later
36
+ ## add (opt) year later
37
+ # e.g. Aug 9 & Aug 10
38
+ ### note - allow shortcut e.g. Aug 9 & 10
39
+ DATE_LEGS_I_RE = %r{
40
+ (?<date_legs>
41
+ \b
42
+ (?<month_name1>#{MONTH_NAMES})
43
+ [ ]
44
+ (?<day1>\d{1,2})
45
+ [ ] & [ ]
46
+ (?:
47
+ (?<month_name2>#{MONTH_NAMES})
48
+ [ ]
49
+ )? ## note - make 2nd month_name optional
50
+ (?<day2>\d{1,2})
51
+ \b
52
+ )}ix
53
+
54
+
55
+ ###
56
+ # e.g. 3 June or 10 June
57
+ ## note - allow more spaces between DAY_NAMES and DAY e.g.
58
+ ## Sun 1 Mar
59
+ ## Wed 4 Mar
60
+ ## Sat 14 Mar
61
+ ## Sat 11 Apr
62
+ ## Sat 11 Apr 2021
63
+ ## Sat 11 Apr 21
64
+ ##
65
+ ## Sat, 11 Apr
66
+ ## note - eat-up optional comma after DAY_NAMES!!
67
+ ##
68
+ ## note - Sat 14 Mar 17:30
69
+ ## check two-digit year (with NEGATIVE lookahead for time!!!)
70
+
71
+ DATE_II_RE = %r{
72
+ (?<date>
73
+ \b
74
+ ## optional day name
75
+ ((?<day_name>#{DAY_NAMES})
76
+ (?: ,?[ ]+)
77
+ )?
78
+ (?<day>\d{1,2})
79
+ [ ]
80
+ (?<month_name>#{MONTH_NAMES})
81
+ \b
82
+ ## optional year
83
+ ( [ ]
84
+ (?:
85
+ (?<year>\d{4}) ## optional year 2025 (yyyy)
86
+ |
87
+ (?:
88
+ (?<yy>\d{2}) ## optional year 25 (yy)
89
+ ## check NEGATIVE lookahead
90
+ (?! :|[:h]\d{2})
91
+ )
92
+ )
93
+ \b
94
+ )?
95
+ )}ix
96
+
97
+
98
+ # e.g. iso-date - 2011-08-25
99
+ ## note - allow/support ("shortcuts") e.g 2011-8-25 or 2011-8-3 / 2011-08-03 etc.
100
+ DATE_III_A_RE = %r{
101
+ (?<date>
102
+ \b
103
+ (?<year>\d{4})
104
+ -
105
+ (?<month>\d{1,2})
106
+ -
107
+ (?<day>\d{1,2})
108
+ \b
109
+ )}ix
110
+
111
+ ## starting w/ day/month/year e.g. 25-08-2011
112
+ DATE_III_B_RE = %r{
113
+ (?<date>
114
+ \b
115
+ ## optional day name
116
+ ((?<day_name>#{DAY_NAMES})
117
+ (?: ,?[ ]+)
118
+ )?
119
+ (?<day>\d{1,2})
120
+ -
121
+ (?<month>\d{1,2})
122
+ -
123
+ (?<year>\d{4})
124
+ \b
125
+ )}ix
126
+
127
+
128
+
129
+ ## allow (short)"european" style 8.8.
130
+ ## note - assume day/month!!!
131
+ DATE_IIII_RE = %r{
132
+ (?<date>
133
+ \b
134
+ ## optional day name
135
+ ((?<day_name>#{DAY_NAMES})
136
+ (?: ,?[ ]+)
137
+ )?
138
+ (?<day>\d{1,2})
139
+ \.
140
+ (?<month>\d{1,2})
141
+ \.
142
+ (?: (?:
143
+ (?<year>\d{4}) ## optional year 2025 (yyyy)
144
+ |
145
+ (?<yy>\d{2}) ## optional year 25 (yy)
146
+ )
147
+ \b
148
+ )?
149
+ )
150
+ }ix
151
+
152
+
153
+ ####################
154
+ ### 04/03/2026 or 4/3/2026
155
+ ## 04/03/26 or 4/3/26
156
+ ## 04/03 or 4/3
157
+ DATE_IIIII_RE = %r{
158
+ (?<date>
159
+ \b
160
+ ## optional day name
161
+ ((?<day_name>#{DAY_NAMES})
162
+ (?: ,?[ ]+)
163
+ )?
164
+ (?<day>\d{1,2})
165
+ /
166
+ (?<month>\d{1,2})
167
+ \b
168
+ (?:
169
+ /
170
+ (?:
171
+ (?<year>\d{4}) ## optional year 2025 (yyyy)
172
+ |
173
+ (?<yy>\d{2}) ## optional year 25 (yy)
174
+ )
175
+ \b
176
+ )?
177
+ )
178
+ }ix
179
+
180
+
181
+
182
+ #############################################
183
+ # map tables
184
+ # note: order matters; first come-first matched/served
185
+ DATE_RE = Regexp.union(
186
+ DATE_I_RE,
187
+ DATE_II_RE,
188
+ DATE_III_A_RE, ## e.g. 1973-08-14
189
+ DATE_III_B_RE,
190
+ DATE_IIII_RE, ## e.g. 8.8. or 8.13.79 or 08.14.1973
191
+ DATE_IIIII_RE, ## e.g. 08/14/1973
192
+ )
193
+
194
+ ## todo - add more format style here; change to Regexp.union later!!!
195
+ DATE_LEGS_RE = DATE_LEGS_I_RE
196
+
197
+
198
+
199
+ end # class Lexer
200
+ end # module Fbtxt