string_to_number 0.1.4 → 0.2.0

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.
@@ -0,0 +1,230 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StringToNumber
4
+ # High-performance French text to number parser
5
+ #
6
+ # This class provides a clean, optimized implementation that maintains
7
+ # compatibility with the original algorithm while adding significant
8
+ # performance improvements through caching and memoization.
9
+ #
10
+ # @example Basic usage
11
+ # parser = StringToNumber::Parser.new
12
+ # parser.parse('vingt et un') #=> 21
13
+ # parser.parse('trois millions') #=> 3_000_000
14
+ #
15
+ # @example Class method usage
16
+ # StringToNumber::Parser.convert('mille deux cent') #=> 1200
17
+ #
18
+ class Parser
19
+ # Import the proven data structures from the original implementation
20
+ WORD_VALUES = StringToNumber::ToNumber::EXCEPTIONS.freeze
21
+ MULTIPLIERS = StringToNumber::ToNumber::POWERS_OF_TEN.freeze
22
+
23
+ # Pre-compiled regex patterns for optimal performance
24
+ MULTIPLIER_KEYS = MULTIPLIERS.keys.reject { |k| %w[un dix].include?(k) }
25
+ .sort_by(&:length).reverse.freeze
26
+ MULTIPLIER_PATTERN = /(?<f>.*?)\s?(?<m>#{MULTIPLIER_KEYS.join('|')})/
27
+ QUATRE_VINGT_PATTERN = /(quatre(-|\s)vingt(s?)((-|\s)dix)?)((-|\s)?)(\w*)/
28
+
29
+ # Cache configuration
30
+ MAX_CACHE_SIZE = 1000
31
+ private_constant :MAX_CACHE_SIZE
32
+
33
+ # Thread-safe class-level caches
34
+ @conversion_cache = {}
35
+ @cache_access_order = []
36
+ @instance_cache = {}
37
+ @cache_mutex = Mutex.new
38
+ @instance_mutex = Mutex.new
39
+
40
+ class << self
41
+ # Convert French text to number using cached parser instance
42
+ #
43
+ # @param text [String] French number text to convert
44
+ # @return [Integer] The numeric value
45
+ # @raise [ArgumentError] if text is not a string
46
+ def convert(text)
47
+ validate_input!(text)
48
+
49
+ normalized = normalize_text(text)
50
+ return 0 if normalized.empty?
51
+
52
+ # Check conversion cache first
53
+ cached_result = get_cached_conversion(normalized)
54
+ return cached_result if cached_result
55
+
56
+ # Get or create parser instance and convert
57
+ parser = get_cached_instance(normalized)
58
+ result = parser.parse_optimized(normalized)
59
+
60
+ # Cache the result
61
+ cache_conversion(normalized, result)
62
+ result
63
+ end
64
+
65
+ # Clear all caches
66
+ def clear_caches!
67
+ @cache_mutex.synchronize do
68
+ @conversion_cache.clear
69
+ @cache_access_order.clear
70
+ end
71
+
72
+ @instance_mutex.synchronize do
73
+ @instance_cache.clear
74
+ end
75
+ end
76
+
77
+ # Get cache statistics
78
+ def cache_stats
79
+ @cache_mutex.synchronize do
80
+ {
81
+ conversion_cache_size: @conversion_cache.size,
82
+ conversion_cache_limit: MAX_CACHE_SIZE,
83
+ instance_cache_size: @instance_cache.size,
84
+ cache_hit_ratio: calculate_hit_ratio
85
+ }
86
+ end
87
+ end
88
+
89
+ private
90
+
91
+ def validate_input!(text)
92
+ raise ArgumentError, 'Input must be a string' unless text.respond_to?(:to_s)
93
+ end
94
+
95
+ def normalize_text(text)
96
+ text.to_s.downcase.strip
97
+ end
98
+
99
+ def get_cached_conversion(normalized_text)
100
+ @cache_mutex.synchronize do
101
+ if @conversion_cache.key?(normalized_text)
102
+ # Update LRU order
103
+ @cache_access_order.delete(normalized_text)
104
+ @cache_access_order.push(normalized_text)
105
+ return @conversion_cache[normalized_text]
106
+ end
107
+ end
108
+ nil
109
+ end
110
+
111
+ def cache_conversion(normalized_text, result)
112
+ @cache_mutex.synchronize do
113
+ # LRU eviction
114
+ if @conversion_cache.size >= MAX_CACHE_SIZE
115
+ oldest = @cache_access_order.shift
116
+ @conversion_cache.delete(oldest)
117
+ end
118
+
119
+ @conversion_cache[normalized_text] = result
120
+ @cache_access_order.push(normalized_text)
121
+ end
122
+ end
123
+
124
+ def get_cached_instance(normalized_text)
125
+ @instance_mutex.synchronize do
126
+ @instance_cache[normalized_text] ||= new(normalized_text)
127
+ end
128
+ end
129
+
130
+ def calculate_hit_ratio
131
+ return 0.0 if @cache_access_order.empty?
132
+ @conversion_cache.size.to_f / @cache_access_order.size
133
+ end
134
+ end
135
+
136
+ # Initialize parser with normalized text
137
+ def initialize(text = '')
138
+ @normalized_text = self.class.send(:normalize_text, text)
139
+ end
140
+
141
+ # Parse the text to numeric value
142
+ def parse
143
+ self.class.convert(@normalized_text)
144
+ end
145
+
146
+ # Internal optimized parsing method using the original proven algorithm
147
+ # but with performance optimizations
148
+ def parse_optimized(text)
149
+ return 0 if text.nil? || text.empty?
150
+
151
+ # Direct lookup (fastest path)
152
+ return WORD_VALUES[text] if WORD_VALUES.key?(text)
153
+
154
+ # Use the proven extraction algorithm from the original implementation
155
+ extract_optimized(text, MULTIPLIER_KEYS.join('|'))
156
+ end
157
+
158
+ private
159
+
160
+ # Optimized version of the original extract method
161
+ # This maintains the exact logic of the working implementation
162
+ # but with performance improvements
163
+ def extract_optimized(sentence, keys, detail: false)
164
+ return 0 if sentence.nil? || sentence.empty?
165
+
166
+ # Direct lookup
167
+ return WORD_VALUES[sentence] if WORD_VALUES.key?(sentence)
168
+
169
+ # Main pattern matching using pre-compiled regex
170
+ if result = MULTIPLIER_PATTERN.match(sentence)
171
+ # Remove matched portion
172
+ sentence = sentence.gsub(result[0], '') if result[0]
173
+
174
+ # Extract factor
175
+ factor = WORD_VALUES[result[:f]] || match_optimized(result[:f])
176
+ factor = 1 if factor.zero? && !detail
177
+ multiple_of_ten = 10**(MULTIPLIERS[result[:m]] || 0)
178
+
179
+ # Handle compound numbers
180
+ if higher_multiple_exists?(result[:m], sentence)
181
+ details = extract_optimized(sentence, keys, detail: true)
182
+ factor = (factor * multiple_of_ten) + details[:factor]
183
+ multiple_of_ten = details[:multiple_of_ten]
184
+ sentence = details[:sentence]
185
+ end
186
+
187
+ # Return based on mode
188
+ if detail
189
+ return {
190
+ factor: factor,
191
+ multiple_of_ten: multiple_of_ten,
192
+ sentence: sentence
193
+ }
194
+ end
195
+
196
+ return extract_optimized(sentence, keys) + factor * multiple_of_ten
197
+
198
+ # Quatre-vingt special handling
199
+ elsif m = QUATRE_VINGT_PATTERN.match(sentence)
200
+ normalize_str = m[1].tr(' ', '-')
201
+ normalize_str = normalize_str[0...-1] if normalize_str[-1] == 's'
202
+
203
+ sentence = sentence.gsub(m[0], '')
204
+
205
+ return extract_optimized(sentence, keys) +
206
+ WORD_VALUES[normalize_str] + (WORD_VALUES[m[8]] || 0)
207
+ else
208
+ return match_optimized(sentence)
209
+ end
210
+ end
211
+
212
+ # Optimized match method
213
+ def match_optimized(sentence)
214
+ return 0 if sentence.nil?
215
+
216
+ sentence.tr('-', ' ').split(' ').reverse.sum do |word|
217
+ next 0 if word == 'et'
218
+ WORD_VALUES[word] || (MULTIPLIERS[word] ? 10 * MULTIPLIERS[word] : 0)
219
+ end
220
+ end
221
+
222
+ # Optimized higher multiple check
223
+ def higher_multiple_exists?(multiple, sentence)
224
+ current_power = MULTIPLIERS[multiple]
225
+ MULTIPLIERS.any? do |word, power|
226
+ power > current_power && sentence.include?(word)
227
+ end
228
+ end
229
+ end
230
+ end
@@ -1,13 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module StringToNumber
4
+ # ToNumber class handles the conversion of French text to numbers
5
+ # It uses a complex recursive parsing algorithm to handle French number grammar
4
6
  class ToNumber
5
7
  attr_accessor :sentence, :keys
6
8
 
9
+ # EXCEPTIONS contains direct mappings from French words to their numeric values
10
+ # This includes:
11
+ # - Basic numbers 0-90
12
+ # - Feminine forms ("une" for "un")
13
+ # - Regional variations (Belgian/Swiss French: "septante", "huitante", "nonante")
14
+ # - Special cases for "quatre-vingt" variations with/without 's'
15
+ # - Compound numbers like "dix-sept", "soixante-dix"
7
16
  EXCEPTIONS = {
8
- 'zéro' => 0,
9
- 'zero' => 0,
10
- 'un' => 1,
17
+ 'zéro' => 0, # Zero with accent
18
+ 'zero' => 0, # Zero without accent
19
+ 'un' => 1, # Masculine "one"
20
+ 'une' => 1, # Feminine "one"
11
21
  'deux' => 2,
12
22
  'trois' => 3,
13
23
  'quatre' => 4,
@@ -23,29 +33,44 @@ module StringToNumber
23
33
  'quatorze' => 14,
24
34
  'quinze' => 15,
25
35
  'seize' => 16,
26
- 'dix-sept' => 17,
27
- 'dix-huit' => 18,
28
- 'dix-neuf' => 19,
36
+ 'dix-sept' => 17, # Compound: "ten-seven"
37
+ 'dix-huit' => 18, # Compound: "ten-eight"
38
+ 'dix-neuf' => 19, # Compound: "ten-nine"
29
39
  'vingt' => 20,
30
40
  'trente' => 30,
31
41
  'quarante' => 40,
32
42
  'cinquante' => 50,
33
43
  'soixante' => 60,
34
- 'soixante-dix' => 70,
35
- 'quatre-vingts' => 80,
36
- 'quatre-vingt' => 80,
37
- 'quatre-vingt-dix' => 90,
38
- 'quatre-vingts-dix' => 90
44
+ 'soixante-dix' => 70, # Standard French: "sixty-ten"
45
+ 'septante' => 70, # Belgian/Swiss French alternative
46
+ 'quatre-vingts' => 80, # Standard French: "four-twenties" (plural)
47
+ 'quatre-vingt' => 80, # Standard French: "four-twenty" (singular)
48
+ 'huitante' => 80, # Swiss French alternative
49
+ 'quatre-vingt-dix' => 90, # Standard French: "four-twenty-ten"
50
+ 'quatre-vingts-dix' => 90,# Alternative with plural "vingts"
51
+ 'nonante' => 90 # Belgian/Swiss French alternative
39
52
  }.freeze
40
53
 
54
+ # POWERS_OF_TEN maps French number words to their power of 10 exponents
55
+ # Used for multipliers like "cent" (10^2), "mille" (10^3), "million" (10^6)
56
+ # Includes both singular and plural forms for proper French grammar
57
+ # Uses French number scale where "billion" = 10^12 (not 10^9 as in English)
41
58
  POWERS_OF_TEN = {
42
- 'un' => 0,
43
- 'dix' => 1,
44
- 'cent' => 2,
45
- 'mille' => 3,
46
- 'million' => 6,
47
- 'billion' => 9,
48
- 'trillion' => 12,
59
+ 'un' => 0, # 10^0 = 1 (ones place)
60
+ 'dix' => 1, # 10^1 = 10 (tens place)
61
+ 'cent' => 2, # 10^2 = 100 (hundreds, singular)
62
+ 'cents' => 2, # 10^2 = 100 (hundreds, plural)
63
+ 'mille' => 3, # 10^3 = 1,000 (thousands, singular)
64
+ 'milles' => 3, # 10^3 = 1,000 (thousands, plural)
65
+ 'million' => 6, # 10^6 = 1,000,000 (millions, singular)
66
+ 'millions' => 6, # 10^6 = 1,000,000 (millions, plural)
67
+ 'milliard' => 9, # 10^9 = 1,000,000,000 (French billion, singular)
68
+ 'milliards' => 9, # 10^9 = 1,000,000,000 (French billion, plural)
69
+ 'billion' => 12, # 10^12 = 1,000,000,000,000 (French trillion, singular)
70
+ 'billions' => 12, # 10^12 = 1,000,000,000,000 (French trillion, plural)
71
+ 'trillion' => 15, # 10^15 (French quadrillion, singular)
72
+ 'trillions' => 15, # 10^15 (French quadrillion, plural)
73
+ # Extended list of large number names for completeness
49
74
  'quadrillion' => 15,
50
75
  'quintillion' => 18,
51
76
  'sextillion' => 21,
@@ -75,42 +100,88 @@ module StringToNumber
75
100
  'trigintillion' => 93,
76
101
  'untrigintillion' => 96,
77
102
  'duotrigintillion' => 99,
78
- 'googol' => 100
103
+ 'googol' => 100 # Special case: 10^100
79
104
  }.freeze
80
105
 
106
+ # Initialize the ToNumber parser with a French sentence
107
+ # @param sentence [String] The French text to be converted to numbers
81
108
  def initialize(sentence = '')
82
- @keys = POWERS_OF_TEN.keys.reject { |k| %w[un dix].include?(k) }.join('|')
83
- @sentence = sentence
109
+ # Create regex pattern from POWERS_OF_TEN keys, excluding 'un' and 'dix'
110
+ # which are handled differently in the parsing logic
111
+ # Sort keys by length (longest first) to ensure longer matches are preferred
112
+ # This prevents "cent" from matching before "cents" in "cinq cents"
113
+ sorted_keys = POWERS_OF_TEN.keys.reject { |k| %w[un dix].include?(k) }.sort_by(&:length).reverse
114
+ @keys = sorted_keys.join('|') # Create regex alternation pattern
115
+ # Normalize input to lowercase for case-insensitive matching
116
+ @sentence = sentence&.downcase || ''
84
117
  end
85
118
 
119
+ # Main entry point to convert the French sentence to a number
120
+ # @return [Integer] The numeric value of the French text
86
121
  def to_number
87
122
  extract(@sentence, keys)
88
123
  end
89
124
 
90
125
  private
91
126
 
127
+ # Main recursive extraction method that parses French number patterns
128
+ # This is the core of the parsing algorithm
129
+ # @param sentence [String] The French text to parse
130
+ # @param keys [String] Regex pattern of power-of-ten multipliers
131
+ # @param detail [Boolean] If true, returns detailed parsing info for recursion
132
+ # @return [Integer, Hash] Numeric value or detailed parsing hash
92
133
  def extract(sentence, keys, detail: false)
134
+ # Base cases: handle empty/nil input
93
135
  return 0 if sentence.nil? || sentence.empty?
136
+
137
+ # Ensure case-insensitive matching
138
+ sentence = sentence.downcase
139
+
140
+ # Direct lookup for simple cases (e.g., "vingt" -> 20)
94
141
  return EXCEPTIONS[sentence] unless EXCEPTIONS[sentence].nil?
95
142
 
143
+ # Main parsing logic: look for pattern "factor + multiplier"
144
+ # Example: "cinq cents" -> factor="cinq", multiplier="cents"
145
+ # Regex explanation:
146
+ # (?<f>.*?) - Non-greedy capture of factor part (before multiplier)
147
+ # \s? - Optional space
148
+ # (?<m>#{keys}) - Named capture of multiplier from keys pattern
96
149
  if result = /(?<f>.*?)\s?(?<m>#{keys})/.match(sentence)
97
- # Deleting matching element
150
+ # Remove the matched portion from sentence for further processing
98
151
  sentence.gsub!($&, '') if $&
99
152
 
100
- # Extract matching element
101
- factor = EXCEPTIONS[result[:f]] || match(result[:f])
102
- factor = 1 if factor.zero?
153
+ # Parse the factor part (number before the multiplier)
154
+ # Example: "cinq" -> 5, "deux cent" -> 200
155
+ factor = EXCEPTIONS[result[:f]] || match(result[:f])
156
+
157
+ # Handle implicit factor of 1 for standalone multipliers
158
+ # Example: "million" -> factor=1, but only for top-level calls
159
+ # For recursive calls (detail=true), keep factor as 0 to avoid double-counting
160
+ factor = 1 if factor.zero? && !detail
161
+
162
+ # Calculate the multiplier value (10^exponent)
163
+ # Example: "cents" -> 10^2 = 100, "millions" -> 10^6 = 1,000,000
103
164
  multiple_of_ten = 10**(POWERS_OF_TEN[result[:m]] || 0)
104
165
 
105
- # Check if this multiple is over
166
+ # Handle compound numbers with higher-order multipliers
167
+ # Example: "cinq cents millions" - after matching "cinq cents",
168
+ # check if "millions" (a higher multiplier than "cents") remains
106
169
  if /#{higher_multiple(result[:m]).keys.join('|')}/.match(sentence)
170
+ # Recursively process the higher multiplier
107
171
  details = extract(sentence, keys, detail: true)
108
172
 
109
- factor = (factor * multiple_of_ten) + details[:factor]
173
+ # Combine the current factor*multiplier with the higher multiplier
174
+ # Example: For "cinq cents millions":
175
+ # - factor = 5, multiple_of_ten = 100 (from "cinq cents")
176
+ # - details[:factor] = 0, details[:multiple_of_ten] = 1000000 (from "millions")
177
+ # - result: factor = (5 * 100) + 0 = 500, multiple_of_ten = 1000000
178
+ # - final: 500 * 1000000 = 500,000,000
179
+ factor = (factor * multiple_of_ten) + details[:factor]
110
180
  multiple_of_ten = details[:multiple_of_ten]
111
- sentence = details[:sentence]
181
+ sentence = details[:sentence]
112
182
  end
113
183
 
184
+ # Return detailed parsing info for recursive calls
114
185
  if detail
115
186
  return {
116
187
  factor: factor,
@@ -119,33 +190,69 @@ module StringToNumber
119
190
  }
120
191
  end
121
192
 
193
+ # Final calculation: process any remaining sentence + current factor*multiplier
194
+ # Example: For "trois millions cinq cents", this handles the "cinq cents" part
122
195
  return extract(sentence, keys) + factor * multiple_of_ten
123
196
 
197
+ # Special case handling for "quatre-vingt" variations
198
+ # This complex regex handles the irregular French "eighty" patterns:
199
+ # - "quatre-vingt" / "quatre vingts" (with/without 's')
200
+ # - "quatre-vingt-dix" / "quatre vingts dix" (90)
201
+ # - Space vs hyphen variations
124
202
  elsif m = /(quatre(-|\s)vingt(s?)((-|\s)dix)?)((-|\s)?)(\w*)/.match(sentence)
203
+ # Normalize spacing to hyphens for consistent lookup
125
204
  normalize_str = m[1].tr(' ', '-')
126
- normalize_str = normalize_str[0...-1] if normalize_str[normalize_str.length] == 's'
205
+
206
+ # Remove trailing 's' from "quatre-vingts" if present
207
+ # Bug fix: use [-1] instead of [length] for last character
208
+ normalize_str = normalize_str[0...-1] if normalize_str[-1] == 's'
127
209
 
210
+ # Remove the matched portion from sentence
128
211
  sentence.gsub!(m[0], '')
129
212
 
213
+ # Return sum of: remaining sentence + normalized quatre-vingt value + any suffix
214
+ # Example: "quatre-vingt-cinq" -> EXCEPTIONS["quatre-vingt"] + EXCEPTIONS["cinq"]
130
215
  return extract(sentence, keys) +
131
216
  EXCEPTIONS[normalize_str] + (EXCEPTIONS[m[8]] || 0)
132
217
  else
218
+ # Fallback: use match() method for simple word combinations
133
219
  return match(sentence)
134
220
  end
135
221
  end
136
222
 
223
+ # Fallback method for parsing simple word sequences
224
+ # Used when the main extract() method can't find multiplier patterns
225
+ # @param sentence [String] French text to parse as individual words
226
+ # @return [Integer, nil] Sum of individual word values or nil if no sentence
137
227
  def match(sentence)
138
228
  return if sentence.nil?
139
229
 
140
- sentence.tr('-', ' ').split(' ').reverse.sum do |word|
230
+ # Process words in reverse order for proper French number logic
231
+ # Example: "vingt et un" -> ["un", "et", "vingt"] -> 1 + 0 + 20 = 21
232
+ sentence.downcase.tr('-', ' ').split(' ').reverse.sum do |word|
233
+ # Handle French "et" (and) conjunction by ignoring it in calculations
234
+ # Example: "vingt et un" -> ignore "et", sum "vingt" + "un"
235
+ next 0 if word == 'et'
236
+
237
+ # Look up word value in either EXCEPTIONS or POWERS_OF_TEN
141
238
  if EXCEPTIONS[word].nil? && POWERS_OF_TEN[word].nil?
239
+ # Unknown words contribute 0 to the sum
142
240
  0
143
241
  else
242
+ # Use EXCEPTIONS value if available, otherwise use 10 * power_of_ten
243
+ # Example: "dix" -> EXCEPTIONS["dix"] = 10
244
+ # "cent" -> 10 * POWERS_OF_TEN["cent"] = 10 * 2 = 100
144
245
  (EXCEPTIONS[word] || (10 * POWERS_OF_TEN[word]))
145
246
  end
146
247
  end
147
248
  end
148
249
 
250
+ # Helper method to find multipliers with higher powers than the given one
251
+ # Used to detect when compound numbers have higher-order multipliers
252
+ # @param multiple [String] The current multiplier word (e.g., "cents")
253
+ # @return [Hash] Hash of multipliers with higher powers of 10
254
+ # Example: higher_multiple("cents") returns {"mille"=>3, "million"=>6, ...}
255
+ # because 10^3, 10^6, etc. are all > 10^2 (cents)
149
256
  def higher_multiple(multiple)
150
257
  POWERS_OF_TEN.select do |_k, v|
151
258
  v > POWERS_OF_TEN[multiple]
@@ -1,3 +1,3 @@
1
1
  module StringToNumber
2
- VERSION = '0.1.4'.freeze
2
+ VERSION = '0.2.0'.freeze
3
3
  end
@@ -1,10 +1,97 @@
1
1
  require 'string_to_number/version'
2
+
3
+ # Load original implementation first for constant definitions
2
4
  require 'string_to_number/to_number'
3
5
 
6
+ # Then load optimized implementation
7
+ require 'string_to_number/parser'
8
+
4
9
  module StringToNumber
10
+ # Main interface for converting French text to numbers
11
+ #
12
+ # This module provides a simple interface to the high-performance French
13
+ # number parser with backward compatibility options.
14
+ #
15
+ # @example Basic usage
16
+ # StringToNumber.in_numbers('vingt et un') #=> 21
17
+ # StringToNumber.in_numbers('trois millions') #=> 3_000_000
18
+ #
19
+ # @example Backward compatibility
20
+ # StringToNumber.in_numbers('cent', use_optimized: false) #=> 100
21
+ #
5
22
  class << self
6
- def in_numbers(sentence)
7
- StringToNumber::ToNumber.new(sentence).to_number
23
+ # Convert French text to number
24
+ #
25
+ # @param sentence [String] French number text to convert
26
+ # @param use_optimized [Boolean] Whether to use optimized parser (default: true)
27
+ # @return [Integer] The numeric value
28
+ # @raise [ArgumentError] if sentence is not convertible to string
29
+ #
30
+ # @example Standard usage
31
+ # in_numbers('vingt et un') #=> 21
32
+ #
33
+ # @example Using original implementation
34
+ # in_numbers('cent', use_optimized: false) #=> 100
35
+ #
36
+ def in_numbers(sentence, use_optimized: true)
37
+ if use_optimized
38
+ Parser.convert(sentence)
39
+ else
40
+ # Fallback to original implementation for compatibility testing
41
+ ToNumber.new(sentence).to_number
42
+ end
43
+ end
44
+
45
+ # Convert using original implementation (for compatibility testing)
46
+ #
47
+ # @param sentence [String] French text to convert
48
+ # @return [Integer] The numeric value
49
+ def in_numbers_original(sentence)
50
+ ToNumber.new(sentence).to_number
51
+ end
52
+
53
+ # Clear all internal caches
54
+ #
55
+ # Useful for testing, memory management, or when processing
56
+ # large volumes of unique inputs.
57
+ #
58
+ # @return [void]
59
+ def clear_caches!
60
+ Parser.clear_caches!
61
+ end
62
+
63
+ # Get cache performance statistics
64
+ #
65
+ # @return [Hash] Cache statistics including sizes and hit ratios
66
+ # @example
67
+ # stats = StringToNumber.cache_stats
68
+ # puts "Cache hit ratio: #{stats[:cache_hit_ratio]}"
69
+ #
70
+ def cache_stats
71
+ Parser.cache_stats
72
+ end
73
+
74
+ # Check if a string contains valid French number words
75
+ #
76
+ # @param text [String] Text to validate
77
+ # @return [Boolean] true if text appears to contain French numbers
78
+ #
79
+ def valid_french_number?(text)
80
+ return false unless text.respond_to?(:to_s)
81
+
82
+ normalized = text.to_s.downcase.strip
83
+ return false if normalized.empty?
84
+
85
+ # Check if any words are recognized French number words
86
+ words = normalized.tr('-', ' ').split(/\s+/)
87
+ recognized_words = words.count do |word|
88
+ word == 'et' ||
89
+ Parser::WORD_VALUES.key?(word) ||
90
+ Parser::MULTIPLIERS.key?(word)
91
+ end
92
+
93
+ # Require at least 50% recognized words for validation
94
+ recognized_words.to_f / words.size >= 0.5
8
95
  end
9
96
  end
10
- end
97
+ end