Module: Lich::Gemstone::Combat::Definitions::PatternGate
- Defined in:
- documented/gemstone/combat/defs/pattern_gate.rb
Constant Summary collapse
- MIN_LITERAL =
Build a gate for a list of patterns. Returns [union_regex, always_scan] where union_regex matches iff some pattern's literal is present, and always_scan lists patterns whose literal was too short to be a useful gate (they must be tried on every line).
4
Class Method Summary collapse
-
.build(patterns) ⇒ Array(Regexp, nil, Array<Regexp>)
Builds a literal-substring gate for a list of patterns to accelerate pattern matching.
-
.longest_literal(regex) ⇒ Object
Longest guaranteed-literal run in a regex source, or nil when no safe literal exists.
-
.rejects?(gate, always_scan, line) ⇒ Boolean
Convenience: true when the line can't possibly match any pattern in this table, so the caller may skip the full scan.
Class Method Details
.build(patterns) ⇒ Array(Regexp, nil, Array<Regexp>)
Builds a literal-substring gate for a list of patterns to accelerate pattern matching.
Extracts the longest guaranteed-literal run from each pattern using longest_literal, then creates a fast Regexp.union gate that rejects lines lacking those substrings before attempting full pattern matches. Patterns with literals shorter than MIN_LITERAL (4 chars) are placed in the always_scan list and must be tried on every line. Returns a tuple that rejects? uses to make reject-or-scan decisions.
84 85 86 87 88 89 90 91 92 93 94 95 96 |
# File 'documented/gemstone/combat/defs/pattern_gate.rb', line 84 def build(patterns) literals = [] always_scan = [] patterns.each do |pattern| literal = longest_literal(pattern) if literal && literal.length >= MIN_LITERAL literals << literal else always_scan << pattern end end [literals.empty? ? nil : Regexp.union(literals.uniq).freeze, always_scan.freeze] end |
.longest_literal(regex) ⇒ Object
Longest guaranteed-literal run in a regex source, or nil when no
safe literal exists. Character classes, escapes and then entire
parenthesized groups (innermost-out, so nesting works) are removed
wholesale - text inside a group may be optional or one alternation
branch, so it is never guaranteed. What survives is top-level text
that every match must contain; the longest metachar-free fragment
of it is the gate literal. A source with a top-level | is a pure
alternation with no guaranteed text - returns nil (always scan).
45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
# File 'documented/gemstone/combat/defs/pattern_gate.rb', line 45 def longest_literal(regex) source = regex.source.dup source.gsub!(/\\[A-Za-z]/, "\x00") # escape sequences (\d, \w, \b...) source.gsub!(/\[[^\]]*\]/, "\x00") # character classes # Remove groups innermost-first so nested groups collapse cleanly nil while source.gsub!(/\((?:\?(?:<[a-zA-Z_]+>|:|=|!))?[^()]*\)/, "\x00") return nil if source.include?('|') # top-level alternation fragments = source.split(/[\\(){}?*+.^$\x00]/) # A fragment followed by ? or * in the original is optional; the # split above already breaks on those metachars, but the char # BEFORE ? belongs to the fragment - trim it to stay conservative. longest = fragments.max_by(&:length).to_s longest = longest[0..-2] if source =~ /#{Regexp.escape(longest)}[?*]/ longest.empty? ? nil : longest end |
.rejects?(gate, always_scan, line) ⇒ Boolean
Convenience: true when the line can't possibly match any pattern in this table, so the caller may skip the full scan. A line is only rejectable when BOTH the literal gate misses AND no ungated (always_scan) pattern matches. Any always_scan pattern that matches keeps the line in play; a non-empty always_scan does NOT blanket- disable rejection (that was the old bug - it reverted the whole table to full-scan the moment one short-literal pattern existed).
105 106 107 108 109 |
# File 'documented/gemstone/combat/defs/pattern_gate.rb', line 105 def rejects?(gate, always_scan, line) return false if gate&.match?(line) always_scan.none? { |rx| rx.match?(line) } end |