string_to_number 0.2.0 → 0.2.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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +83 -0
- data/.rubocop.yml +110 -0
- data/Gemfile +9 -0
- data/Gemfile.lock +32 -1
- data/README.md +9 -5
- data/Rakefile +5 -1
- data/benchmark.rb +41 -40
- data/lib/string_to_number/parser.rb +20 -18
- data/lib/string_to_number/to_number.rb +20 -20
- data/lib/string_to_number/version.rb +3 -1
- data/lib/string_to_number.rb +9 -7
- data/logo.png +0 -0
- data/microbenchmark.rb +81 -80
- data/performance_comparison.rb +34 -35
- data/profile.rb +44 -45
- data/string_to_number.gemspec +5 -6
- metadata +7 -45
@@ -47,8 +47,8 @@ module StringToNumber
|
|
47
47
|
'quatre-vingt' => 80, # Standard French: "four-twenty" (singular)
|
48
48
|
'huitante' => 80, # Swiss French alternative
|
49
49
|
'quatre-vingt-dix' => 90, # Standard French: "four-twenty-ten"
|
50
|
-
'quatre-vingts-dix' => 90
|
51
|
-
'nonante' => 90
|
50
|
+
'quatre-vingts-dix' => 90, # Alternative with plural "vingts"
|
51
|
+
'nonante' => 90 # Belgian/Swiss French alternative
|
52
52
|
}.freeze
|
53
53
|
|
54
54
|
# POWERS_OF_TEN maps French number words to their power of 10 exponents
|
@@ -100,7 +100,7 @@ module StringToNumber
|
|
100
100
|
'trigintillion' => 93,
|
101
101
|
'untrigintillion' => 96,
|
102
102
|
'duotrigintillion' => 99,
|
103
|
-
'googol' => 100
|
103
|
+
'googol' => 100 # Special case: 10^100
|
104
104
|
}.freeze
|
105
105
|
|
106
106
|
# Initialize the ToNumber parser with a French sentence
|
@@ -111,7 +111,7 @@ module StringToNumber
|
|
111
111
|
# Sort keys by length (longest first) to ensure longer matches are preferred
|
112
112
|
# This prevents "cent" from matching before "cents" in "cinq cents"
|
113
113
|
sorted_keys = POWERS_OF_TEN.keys.reject { |k| %w[un dix].include?(k) }.sort_by(&:length).reverse
|
114
|
-
@keys = sorted_keys.join('|')
|
114
|
+
@keys = sorted_keys.join('|') # Create regex alternation pattern
|
115
115
|
# Normalize input to lowercase for case-insensitive matching
|
116
116
|
@sentence = sentence&.downcase || ''
|
117
117
|
end
|
@@ -133,10 +133,10 @@ module StringToNumber
|
|
133
133
|
def extract(sentence, keys, detail: false)
|
134
134
|
# Base cases: handle empty/nil input
|
135
135
|
return 0 if sentence.nil? || sentence.empty?
|
136
|
-
|
136
|
+
|
137
137
|
# Ensure case-insensitive matching
|
138
138
|
sentence = sentence.downcase
|
139
|
-
|
139
|
+
|
140
140
|
# Direct lookup for simple cases (e.g., "vingt" -> 20)
|
141
141
|
return EXCEPTIONS[sentence] unless EXCEPTIONS[sentence].nil?
|
142
142
|
|
@@ -146,19 +146,19 @@ module StringToNumber
|
|
146
146
|
# (?<f>.*?) - Non-greedy capture of factor part (before multiplier)
|
147
147
|
# \s? - Optional space
|
148
148
|
# (?<m>#{keys}) - Named capture of multiplier from keys pattern
|
149
|
-
if result = /(?<f>.*?)\s?(?<m>#{keys})/.match(sentence)
|
149
|
+
if (result = /(?<f>.*?)\s?(?<m>#{keys})/.match(sentence))
|
150
150
|
# Remove the matched portion from sentence for further processing
|
151
|
-
sentence.gsub!(
|
151
|
+
sentence.gsub!(::Regexp.last_match(0), '') if ::Regexp.last_match(0)
|
152
152
|
|
153
153
|
# Parse the factor part (number before the multiplier)
|
154
154
|
# Example: "cinq" -> 5, "deux cent" -> 200
|
155
155
|
factor = EXCEPTIONS[result[:f]] || match(result[:f])
|
156
|
-
|
156
|
+
|
157
157
|
# Handle implicit factor of 1 for standalone multipliers
|
158
158
|
# Example: "million" -> factor=1, but only for top-level calls
|
159
159
|
# For recursive calls (detail=true), keep factor as 0 to avoid double-counting
|
160
160
|
factor = 1 if factor.zero? && !detail
|
161
|
-
|
161
|
+
|
162
162
|
# Calculate the multiplier value (10^exponent)
|
163
163
|
# Example: "cents" -> 10^2 = 100, "millions" -> 10^6 = 1,000,000
|
164
164
|
multiple_of_ten = 10**(POWERS_OF_TEN[result[:m]] || 0)
|
@@ -192,17 +192,17 @@ module StringToNumber
|
|
192
192
|
|
193
193
|
# Final calculation: process any remaining sentence + current factor*multiplier
|
194
194
|
# Example: For "trois millions cinq cents", this handles the "cinq cents" part
|
195
|
-
|
195
|
+
extract(sentence, keys) + (factor * multiple_of_ten)
|
196
196
|
|
197
197
|
# Special case handling for "quatre-vingt" variations
|
198
198
|
# This complex regex handles the irregular French "eighty" patterns:
|
199
199
|
# - "quatre-vingt" / "quatre vingts" (with/without 's')
|
200
200
|
# - "quatre-vingt-dix" / "quatre vingts dix" (90)
|
201
201
|
# - Space vs hyphen variations
|
202
|
-
elsif m = /(quatre(-|\s)vingt(s?)((-|\s)dix)?)((-|\s)?)(\w*)/.match(sentence)
|
202
|
+
elsif (m = /(quatre(-|\s)vingt(s?)((-|\s)dix)?)((-|\s)?)(\w*)/.match(sentence))
|
203
203
|
# Normalize spacing to hyphens for consistent lookup
|
204
204
|
normalize_str = m[1].tr(' ', '-')
|
205
|
-
|
205
|
+
|
206
206
|
# Remove trailing 's' from "quatre-vingts" if present
|
207
207
|
# Bug fix: use [-1] instead of [length] for last character
|
208
208
|
normalize_str = normalize_str[0...-1] if normalize_str[-1] == 's'
|
@@ -212,11 +212,11 @@ module StringToNumber
|
|
212
212
|
|
213
213
|
# Return sum of: remaining sentence + normalized quatre-vingt value + any suffix
|
214
214
|
# Example: "quatre-vingt-cinq" -> EXCEPTIONS["quatre-vingt"] + EXCEPTIONS["cinq"]
|
215
|
-
|
216
|
-
|
215
|
+
extract(sentence, keys) +
|
216
|
+
EXCEPTIONS[normalize_str] + (EXCEPTIONS[m[8]] || 0)
|
217
217
|
else
|
218
218
|
# Fallback: use match() method for simple word combinations
|
219
|
-
|
219
|
+
match(sentence)
|
220
220
|
end
|
221
221
|
end
|
222
222
|
|
@@ -229,11 +229,11 @@ module StringToNumber
|
|
229
229
|
|
230
230
|
# Process words in reverse order for proper French number logic
|
231
231
|
# Example: "vingt et un" -> ["un", "et", "vingt"] -> 1 + 0 + 20 = 21
|
232
|
-
sentence.downcase.tr('-', ' ').split
|
232
|
+
sentence.downcase.tr('-', ' ').split.reverse.sum do |word|
|
233
233
|
# Handle French "et" (and) conjunction by ignoring it in calculations
|
234
234
|
# Example: "vingt et un" -> ignore "et", sum "vingt" + "un"
|
235
235
|
next 0 if word == 'et'
|
236
|
-
|
236
|
+
|
237
237
|
# Look up word value in either EXCEPTIONS or POWERS_OF_TEN
|
238
238
|
if EXCEPTIONS[word].nil? && POWERS_OF_TEN[word].nil?
|
239
239
|
# Unknown words contribute 0 to the sum
|
@@ -241,8 +241,8 @@ module StringToNumber
|
|
241
241
|
else
|
242
242
|
# Use EXCEPTIONS value if available, otherwise use 10 * power_of_ten
|
243
243
|
# Example: "dix" -> EXCEPTIONS["dix"] = 10
|
244
|
-
# "cent" -> 10 * POWERS_OF_TEN["cent"] = 10 * 2 = 100
|
245
|
-
|
244
|
+
# "cent" -> 10 * POWERS_OF_TEN["cent"] = 10 * 2 = 100
|
245
|
+
EXCEPTIONS[word] || (10 * POWERS_OF_TEN[word])
|
246
246
|
end
|
247
247
|
end
|
248
248
|
end
|
data/lib/string_to_number.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'string_to_number/version'
|
2
4
|
|
3
5
|
# Load original implementation first for constant definitions
|
@@ -78,20 +80,20 @@ module StringToNumber
|
|
78
80
|
#
|
79
81
|
def valid_french_number?(text)
|
80
82
|
return false unless text.respond_to?(:to_s)
|
81
|
-
|
83
|
+
|
82
84
|
normalized = text.to_s.downcase.strip
|
83
85
|
return false if normalized.empty?
|
84
|
-
|
86
|
+
|
85
87
|
# Check if any words are recognized French number words
|
86
88
|
words = normalized.tr('-', ' ').split(/\s+/)
|
87
89
|
recognized_words = words.count do |word|
|
88
|
-
word == 'et' ||
|
89
|
-
|
90
|
-
|
90
|
+
word == 'et' ||
|
91
|
+
Parser::WORD_VALUES.key?(word) ||
|
92
|
+
Parser::MULTIPLIERS.key?(word)
|
91
93
|
end
|
92
|
-
|
94
|
+
|
93
95
|
# Require at least 50% recognized words for validation
|
94
96
|
recognized_words.to_f / words.size >= 0.5
|
95
97
|
end
|
96
98
|
end
|
97
|
-
end
|
99
|
+
end
|
data/logo.png
ADDED
Binary file
|
data/microbenchmark.rb
CHANGED
@@ -9,10 +9,10 @@ require 'benchmark'
|
|
9
9
|
|
10
10
|
class MicroBenchmark
|
11
11
|
def self.run
|
12
|
-
puts
|
13
|
-
puts
|
12
|
+
puts 'StringToNumber Micro-Benchmarks'
|
13
|
+
puts '=' * 50
|
14
14
|
puts
|
15
|
-
|
15
|
+
|
16
16
|
# Test individual components
|
17
17
|
test_initialization
|
18
18
|
test_regex_compilation
|
@@ -20,43 +20,45 @@ class MicroBenchmark
|
|
20
20
|
test_hash_lookups
|
21
21
|
test_string_operations
|
22
22
|
test_recursion_overhead
|
23
|
-
|
23
|
+
|
24
24
|
puts "\nConclusions and Recommendations:"
|
25
|
-
puts
|
25
|
+
puts '=' * 50
|
26
26
|
analyze_results
|
27
27
|
end
|
28
28
|
|
29
29
|
def self.test_initialization
|
30
|
-
puts
|
31
|
-
puts
|
32
|
-
|
30
|
+
puts '1. Initialization Performance'
|
31
|
+
puts '-' * 30
|
32
|
+
|
33
33
|
# Test the cost of creating new instances
|
34
34
|
sentences = ['un', 'vingt et un', 'mille deux cent', 'trois milliards cinq cents millions']
|
35
|
-
|
35
|
+
|
36
36
|
sentences.each do |sentence|
|
37
37
|
time = Benchmark.realtime do
|
38
38
|
1000.times { StringToNumber::ToNumber.new(sentence) }
|
39
39
|
end
|
40
|
-
|
40
|
+
|
41
41
|
puts "#{sentence.ljust(35)}: #{(time * 1000).round(4)}ms per 1000 instances"
|
42
42
|
end
|
43
43
|
puts
|
44
44
|
end
|
45
45
|
|
46
46
|
def self.test_regex_compilation
|
47
|
-
puts
|
48
|
-
puts
|
49
|
-
|
47
|
+
puts '2. Regex Compilation Performance'
|
48
|
+
puts '-' * 30
|
49
|
+
|
50
50
|
# Test the cost of regex compilation vs pre-compiled regex
|
51
|
-
keys = StringToNumber::ToNumber::POWERS_OF_TEN.keys.reject
|
52
|
-
|
51
|
+
keys = StringToNumber::ToNumber::POWERS_OF_TEN.keys.reject do |k|
|
52
|
+
%w[un dix].include?(k)
|
53
|
+
end.sort_by(&:length).reverse.join('|')
|
54
|
+
|
53
55
|
# Dynamic compilation
|
54
56
|
dynamic_time = Benchmark.realtime do
|
55
57
|
1000.times do
|
56
58
|
/(?<f>.*?)\s?(?<m>#{keys})/.match('trois milliards')
|
57
59
|
end
|
58
60
|
end
|
59
|
-
|
61
|
+
|
60
62
|
# Pre-compiled regex
|
61
63
|
compiled_regex = /(?<f>.*?)\s?(?<m>#{Regexp.escape(keys)})/
|
62
64
|
precompiled_time = Benchmark.realtime do
|
@@ -64,7 +66,7 @@ class MicroBenchmark
|
|
64
66
|
compiled_regex.match('trois milliards')
|
65
67
|
end
|
66
68
|
end
|
67
|
-
|
69
|
+
|
68
70
|
puts "Dynamic regex compilation: #{(dynamic_time * 1000).round(4)}ms per 1000 matches"
|
69
71
|
puts "Pre-compiled regex: #{(precompiled_time * 1000).round(4)}ms per 1000 matches"
|
70
72
|
puts "Compilation overhead: #{((dynamic_time - precompiled_time) * 1000).round(4)}ms per 1000 matches"
|
@@ -72,62 +74,64 @@ class MicroBenchmark
|
|
72
74
|
end
|
73
75
|
|
74
76
|
def self.test_regex_matching
|
75
|
-
puts
|
76
|
-
puts
|
77
|
-
|
77
|
+
puts '3. Regex Pattern Complexity'
|
78
|
+
puts '-' * 30
|
79
|
+
|
78
80
|
# Test different regex patterns to see which are expensive
|
79
81
|
test_patterns = {
|
80
82
|
'Simple word match' => /vingt/,
|
81
83
|
'Word boundary match' => /\bvingt\b/,
|
82
84
|
'Named capture groups' => /(?<f>.*?)\s?(?<m>vingt)/,
|
83
85
|
'Complex alternation' => /(?<f>.*?)\s?(?<m>vingt|trente|quarante|cinquante)/,
|
84
|
-
'Full keys pattern' => /(?<f>.*?)\s?(?<m>#{StringToNumber::ToNumber::POWERS_OF_TEN.keys.reject
|
86
|
+
'Full keys pattern' => /(?<f>.*?)\s?(?<m>#{StringToNumber::ToNumber::POWERS_OF_TEN.keys.reject do |k|
|
87
|
+
%w[un dix].include?(k)
|
88
|
+
end.sort_by(&:length).reverse.join('|')})/
|
85
89
|
}
|
86
|
-
|
90
|
+
|
87
91
|
test_string = 'trois milliards cinq cents millions'
|
88
|
-
|
92
|
+
|
89
93
|
test_patterns.each do |name, pattern|
|
90
94
|
time = Benchmark.realtime do
|
91
95
|
5000.times { pattern.match(test_string) }
|
92
96
|
end
|
93
|
-
|
97
|
+
|
94
98
|
puts "#{name.ljust(25)}: #{(time * 1000).round(4)}ms per 5000 matches"
|
95
99
|
end
|
96
100
|
puts
|
97
101
|
end
|
98
102
|
|
99
103
|
def self.test_hash_lookups
|
100
|
-
puts
|
101
|
-
puts
|
102
|
-
|
104
|
+
puts '4. Hash Lookup Performance'
|
105
|
+
puts '-' * 30
|
106
|
+
|
103
107
|
exceptions = StringToNumber::ToNumber::EXCEPTIONS
|
104
108
|
powers = StringToNumber::ToNumber::POWERS_OF_TEN
|
105
|
-
|
109
|
+
|
106
110
|
# Test lookup performance
|
107
111
|
exceptions_time = Benchmark.realtime do
|
108
|
-
|
112
|
+
10_000.times do
|
109
113
|
exceptions['vingt']
|
110
114
|
exceptions['trois']
|
111
115
|
exceptions['cent']
|
112
116
|
end
|
113
117
|
end
|
114
|
-
|
118
|
+
|
115
119
|
powers_time = Benchmark.realtime do
|
116
|
-
|
120
|
+
10_000.times do
|
117
121
|
powers['million']
|
118
122
|
powers['mille']
|
119
123
|
powers['cent']
|
120
124
|
end
|
121
125
|
end
|
122
|
-
|
126
|
+
|
123
127
|
# Test nil checks
|
124
128
|
nil_check_time = Benchmark.realtime do
|
125
|
-
|
129
|
+
10_000.times do
|
126
130
|
exceptions['nonexistent'].nil?
|
127
131
|
powers['nonexistent'].nil?
|
128
132
|
end
|
129
133
|
end
|
130
|
-
|
134
|
+
|
131
135
|
puts "EXCEPTIONS hash lookups: #{(exceptions_time * 100).round(4)}ms per 10000 lookups"
|
132
136
|
puts "POWERS_OF_TEN hash lookups: #{(powers_time * 100).round(4)}ms per 10000 lookups"
|
133
137
|
puts "Nil check operations: #{(nil_check_time * 100).round(4)}ms per 10000 checks"
|
@@ -135,28 +139,28 @@ class MicroBenchmark
|
|
135
139
|
end
|
136
140
|
|
137
141
|
def self.test_string_operations
|
138
|
-
puts
|
139
|
-
puts
|
140
|
-
|
142
|
+
puts '5. String Operations Performance'
|
143
|
+
puts '-' * 30
|
144
|
+
|
141
145
|
test_string = 'TROIS MILLIARDS CINQ CENTS MILLIONS'
|
142
|
-
|
146
|
+
|
143
147
|
# Test different string operations
|
144
148
|
downcase_time = Benchmark.realtime do
|
145
149
|
5000.times { test_string.downcase }
|
146
150
|
end
|
147
|
-
|
151
|
+
|
148
152
|
gsub_time = Benchmark.realtime do
|
149
|
-
5000.times { test_string.gsub(
|
153
|
+
5000.times { test_string.gsub('MILLIONS', '') }
|
150
154
|
end
|
151
|
-
|
155
|
+
|
152
156
|
split_time = Benchmark.realtime do
|
153
|
-
5000.times { test_string.split
|
157
|
+
5000.times { test_string.split }
|
154
158
|
end
|
155
|
-
|
159
|
+
|
156
160
|
tr_time = Benchmark.realtime do
|
157
161
|
5000.times { test_string.tr('-', ' ') }
|
158
162
|
end
|
159
|
-
|
163
|
+
|
160
164
|
puts "String#downcase: #{(downcase_time * 1000).round(4)}ms per 5000 operations"
|
161
165
|
puts "String#gsub: #{(gsub_time * 1000).round(4)}ms per 5000 operations"
|
162
166
|
puts "String#split: #{(split_time * 1000).round(4)}ms per 5000 operations"
|
@@ -165,29 +169,28 @@ class MicroBenchmark
|
|
165
169
|
end
|
166
170
|
|
167
171
|
def self.test_recursion_overhead
|
168
|
-
puts
|
169
|
-
puts
|
170
|
-
|
172
|
+
puts '6. Recursion vs Iteration Performance'
|
173
|
+
puts '-' * 30
|
174
|
+
|
171
175
|
# Compare recursive vs iterative approaches
|
172
|
-
|
176
|
+
recursive_sum = lambda do |arr, index = 0|
|
173
177
|
return 0 if index >= arr.length
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
def self.iterative_sum(arr)
|
178
|
-
arr.sum
|
178
|
+
|
179
|
+
arr[index] + recursive_sum.call(arr, index + 1)
|
179
180
|
end
|
180
|
-
|
181
|
+
|
182
|
+
iterative_sum = :sum.to_proc
|
183
|
+
|
181
184
|
test_array = Array.new(100) { rand(100) }
|
182
|
-
|
185
|
+
|
183
186
|
recursive_time = Benchmark.realtime do
|
184
|
-
1000.times { recursive_sum(test_array) }
|
187
|
+
1000.times { recursive_sum.call(test_array) }
|
185
188
|
end
|
186
|
-
|
189
|
+
|
187
190
|
iterative_time = Benchmark.realtime do
|
188
|
-
1000.times { iterative_sum(test_array) }
|
191
|
+
1000.times { iterative_sum.call(test_array) }
|
189
192
|
end
|
190
|
-
|
193
|
+
|
191
194
|
puts "Recursive approach: #{(recursive_time * 1000).round(4)}ms per 1000 operations"
|
192
195
|
puts "Iterative approach: #{(iterative_time * 1000).round(4)}ms per 1000 operations"
|
193
196
|
puts "Recursion overhead: #{((recursive_time - iterative_time) * 1000).round(4)}ms per 1000 operations"
|
@@ -195,32 +198,30 @@ class MicroBenchmark
|
|
195
198
|
end
|
196
199
|
|
197
200
|
def self.analyze_results
|
198
|
-
puts
|
201
|
+
puts 'Key Performance Insights:'
|
199
202
|
puts
|
200
|
-
puts
|
201
|
-
puts
|
202
|
-
puts
|
203
|
+
puts '1. 🔍 INITIALIZATION COST:'
|
204
|
+
puts ' - Creating new ToNumber instances is expensive (~13ms per 1000)'
|
205
|
+
puts ' - Consider caching or singleton pattern for repeated use'
|
203
206
|
puts
|
204
|
-
puts
|
205
|
-
puts
|
206
|
-
puts
|
207
|
-
puts
|
207
|
+
puts '2. 🔍 REGEX COMPLEXITY:'
|
208
|
+
puts ' - Complex alternation patterns are the main bottleneck'
|
209
|
+
puts ' - Keys pattern is 521 characters long - very expensive to match'
|
210
|
+
puts ' - Consider breaking down into simpler patterns or using different approach'
|
208
211
|
puts
|
209
|
-
puts
|
210
|
-
puts
|
211
|
-
puts
|
212
|
-
puts
|
212
|
+
puts '3. 🔍 SCALABILITY ISSUES:'
|
213
|
+
puts ' - Performance degrades significantly with input length (43x for longest)'
|
214
|
+
puts ' - Recursive parsing creates overhead for complex numbers'
|
215
|
+
puts ' - String operations add up with multiple passes'
|
213
216
|
puts
|
214
|
-
puts
|
215
|
-
puts
|
216
|
-
puts
|
217
|
-
puts
|
218
|
-
puts
|
219
|
-
puts
|
217
|
+
puts '📊 OPTIMIZATION RECOMMENDATIONS:'
|
218
|
+
puts ' 1. Pre-compile regex patterns in class constants'
|
219
|
+
puts ' 2. Use simpler regex patterns with multiple passes if needed'
|
220
|
+
puts ' 3. Implement caching for repeated conversions'
|
221
|
+
puts ' 4. Consider iterative parsing instead of recursive for complex cases'
|
222
|
+
puts ' 5. Optimize string operations (minimize downcase/gsub calls)'
|
220
223
|
end
|
221
224
|
end
|
222
225
|
|
223
226
|
# Run the micro-benchmarks
|
224
|
-
if __FILE__ == $
|
225
|
-
MicroBenchmark.run
|
226
|
-
end
|
227
|
+
MicroBenchmark.run if __FILE__ == $PROGRAM_NAME
|
data/performance_comparison.rb
CHANGED
@@ -16,20 +16,20 @@ class PerformanceComparison
|
|
16
16
|
].freeze
|
17
17
|
|
18
18
|
def self.run_comparison
|
19
|
-
puts
|
20
|
-
puts
|
21
|
-
puts
|
22
|
-
puts
|
19
|
+
puts 'StringToNumber Performance Comparison'
|
20
|
+
puts '=' * 60
|
21
|
+
puts 'Original vs Optimized Implementation'
|
22
|
+
puts '=' * 60
|
23
23
|
puts
|
24
24
|
|
25
25
|
TEST_CASES.each_with_index do |test_case, index|
|
26
26
|
puts "Test #{index + 1}: '#{test_case}'"
|
27
|
-
puts
|
27
|
+
puts '-' * 50
|
28
28
|
|
29
29
|
# Verify both implementations produce same results
|
30
30
|
original_result = StringToNumber.in_numbers(test_case, use_optimized: false)
|
31
31
|
optimized_result = StringToNumber.in_numbers(test_case, use_optimized: true)
|
32
|
-
|
32
|
+
|
33
33
|
if original_result == optimized_result
|
34
34
|
puts "✅ Results match: #{original_result}"
|
35
35
|
else
|
@@ -38,7 +38,7 @@ class PerformanceComparison
|
|
38
38
|
end
|
39
39
|
|
40
40
|
# Benchmark both implementations
|
41
|
-
iterations =
|
41
|
+
iterations = 10_000
|
42
42
|
|
43
43
|
original_time = Benchmark.realtime do
|
44
44
|
iterations.times { StringToNumber.in_numbers(test_case, use_optimized: false) }
|
@@ -55,27 +55,27 @@ class PerformanceComparison
|
|
55
55
|
puts "Original: #{original_avg.round(4)}ms average"
|
56
56
|
puts "Optimized: #{optimized_avg.round(4)}ms average"
|
57
57
|
puts "Speedup: #{speedup.round(1)}x faster"
|
58
|
-
|
58
|
+
|
59
59
|
# Performance rating
|
60
60
|
rating = case speedup
|
61
|
-
when 0..2 then
|
62
|
-
when 2..10 then
|
63
|
-
when 10..50 then
|
64
|
-
else
|
61
|
+
when 0..2 then '🟡 Minor improvement'
|
62
|
+
when 2..10 then '🟢 Good improvement'
|
63
|
+
when 10..50 then '🟢 Great improvement'
|
64
|
+
else '🚀 Exceptional improvement'
|
65
65
|
end
|
66
|
-
|
66
|
+
|
67
67
|
puts "Rating: #{rating}"
|
68
68
|
puts
|
69
69
|
end
|
70
70
|
|
71
71
|
# Overall comparison
|
72
|
-
puts
|
73
|
-
puts
|
74
|
-
puts
|
72
|
+
puts '=' * 60
|
73
|
+
puts 'OVERALL PERFORMANCE ANALYSIS'
|
74
|
+
puts '=' * 60
|
75
75
|
|
76
76
|
# Test cache performance
|
77
77
|
puts "\nCache Performance Test:"
|
78
|
-
puts
|
78
|
+
puts '-' * 30
|
79
79
|
|
80
80
|
# Clear caches
|
81
81
|
StringToNumber.clear_caches!
|
@@ -107,17 +107,17 @@ class PerformanceComparison
|
|
107
107
|
|
108
108
|
# Scalability test
|
109
109
|
puts "\nScalability Comparison:"
|
110
|
-
puts
|
110
|
+
puts '-' * 30
|
111
111
|
|
112
112
|
scalability_tests = [
|
113
113
|
'un', # 2 chars
|
114
114
|
'vingt et un', # 11 chars
|
115
|
-
'mille deux cent trente-quatre',
|
115
|
+
'mille deux cent trente-quatre', # 29 chars
|
116
116
|
'soixante-quinze million trois cent quarante six mille sept cent quatre-vingt-dix neuf' # 85 chars
|
117
117
|
]
|
118
118
|
|
119
|
-
puts
|
120
|
-
puts
|
119
|
+
puts 'Input Length | Original | Optimized | Improvement'
|
120
|
+
puts '-------------|----------|-----------|------------'
|
121
121
|
|
122
122
|
scalability_tests.each do |test|
|
123
123
|
original_time = Benchmark.realtime do
|
@@ -132,24 +132,23 @@ class PerformanceComparison
|
|
132
132
|
optimized_ms = (optimized_time / 1000) * 1000
|
133
133
|
improvement = original_ms / optimized_ms
|
134
134
|
|
135
|
-
puts "#{test.length.to_s.rjust(11)} | #{original_ms.round(4).to_s.rjust(8)} |
|
135
|
+
puts "#{test.length.to_s.rjust(11)} | #{original_ms.round(4).to_s.rjust(8)} | " \
|
136
|
+
"#{optimized_ms.round(4).to_s.rjust(9)} | #{improvement.round(1).to_s.rjust(10)}x"
|
136
137
|
end
|
137
138
|
|
138
|
-
puts "\n
|
139
|
-
puts
|
140
|
-
puts
|
141
|
-
puts
|
142
|
-
puts
|
143
|
-
puts
|
144
|
-
puts
|
145
|
-
puts
|
139
|
+
puts "\n#{'=' * 60}"
|
140
|
+
puts 'SUMMARY'
|
141
|
+
puts '=' * 60
|
142
|
+
puts '✅ All test cases produce identical results'
|
143
|
+
puts '🚀 Significant performance improvements across all test cases'
|
144
|
+
puts '📈 Better scalability with input length'
|
145
|
+
puts '💾 Effective caching reduces repeated conversion time'
|
146
|
+
puts '🧠 Lower memory usage and object creation'
|
146
147
|
puts
|
147
|
-
puts
|
148
|
-
puts
|
148
|
+
puts 'The optimized implementation successfully addresses all identified'
|
149
|
+
puts 'performance bottlenecks while maintaining full compatibility.'
|
149
150
|
end
|
150
151
|
end
|
151
152
|
|
152
153
|
# Run the comparison
|
153
|
-
if __FILE__ == $
|
154
|
-
PerformanceComparison.run_comparison
|
155
|
-
end
|
154
|
+
PerformanceComparison.run_comparison if __FILE__ == $PROGRAM_NAME
|