midwire_common 1.1.0 → 2.0.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.
Files changed (48) hide show
  1. checksums.yaml +5 -5
  2. data/.github/mergeable.yml +43 -0
  3. data/.gitignore +23 -0
  4. data/.ruby-version +1 -1
  5. data/CHANGELOG +4 -0
  6. data/CLAUDE.md +95 -0
  7. data/Gemfile +2 -0
  8. data/README.md +8 -5
  9. data/Rakefile +3 -1
  10. data/docs/plans/2026-02-13-refinements-implementation.md +1494 -0
  11. data/docs/plans/2026-02-13-refinements-modernization-design.md +109 -0
  12. data/lib/midwire_common/all.rb +16 -1
  13. data/lib/midwire_common/array.rb +35 -45
  14. data/lib/midwire_common/data_file_cache.rb +6 -5
  15. data/lib/midwire_common/enumerable.rb +12 -8
  16. data/lib/midwire_common/file.rb +13 -1
  17. data/lib/midwire_common/float.rb +10 -3
  18. data/lib/midwire_common/hash.rb +94 -105
  19. data/lib/midwire_common/integer.rb +11 -0
  20. data/lib/midwire_common/number_behavior.rb +4 -2
  21. data/lib/midwire_common/rake_helper.rb +5 -3
  22. data/lib/midwire_common/rake_tasks.rb +2 -0
  23. data/lib/midwire_common/string.rb +76 -108
  24. data/lib/midwire_common/time.rb +10 -6
  25. data/lib/midwire_common/time_tool.rb +6 -2
  26. data/lib/midwire_common/version.rb +3 -1
  27. data/lib/midwire_common/yaml_setting.rb +4 -3
  28. data/lib/midwire_common.rb +8 -2
  29. data/lib/tasks/version.rake +21 -18
  30. data/midwire_common.gemspec +10 -13
  31. data/spec/lib/midwire_common/array_spec.rb +23 -23
  32. data/spec/lib/midwire_common/data_file_cache_spec.rb +14 -14
  33. data/spec/lib/midwire_common/enumerable_spec.rb +8 -4
  34. data/spec/lib/midwire_common/file/stat_spec.rb +8 -4
  35. data/spec/lib/midwire_common/float_spec.rb +7 -3
  36. data/spec/lib/midwire_common/hash_spec.rb +55 -24
  37. data/spec/lib/midwire_common/integer_spec.rb +11 -0
  38. data/spec/lib/midwire_common/rake_helper_spec.rb +6 -3
  39. data/spec/lib/midwire_common/string_spec.rb +47 -77
  40. data/spec/lib/midwire_common/time_spec.rb +19 -20
  41. data/spec/lib/midwire_common/time_tool_spec.rb +4 -2
  42. data/spec/lib/midwire_common/yaml_setting_spec.rb +8 -5
  43. data/spec/spec_helper.rb +18 -12
  44. metadata +29 -99
  45. data/Guardfile +0 -14
  46. data/lib/midwire_common/file/stat.rb +0 -11
  47. data/lib/midwire_common/fixnum.rb +0 -4
  48. data/spec/lib/midwire_common/fixnum_spec.rb +0 -15
@@ -0,0 +1,1494 @@
1
+ # midwire_common 2.0.0 Refinements Implementation Plan
2
+
3
+ > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
4
+
5
+ **Goal:** Replace all monkey-patching with Ruby refinements, modernize all dependencies, remove stdlib-overlapping methods, and release as version 2.0.0.
6
+
7
+ **Architecture:** Each monkey-patched file becomes a refinement module (`MidwireCommon::XxxExtensions`) using Ruby's `refine` blocks. Consumers opt in per-file with `using`. A composed `MidwireCommon::All` module includes all refinement modules so `using MidwireCommon::All` activates everything at once.
8
+
9
+ **Tech Stack:** Ruby >= 3.2, RSpec ~> 3.12, Rake ~> 13.0
10
+
11
+ **Cross-dependency note:** Some specs depend on methods from other modules. Specifically:
12
+ - `array_spec.rb` uses `String#here_with_pipe` (from StringExtensions)
13
+ - `time_spec.rb` uses `String#numeric?` (from StringExtensions)
14
+ - `version.rake` uses `String#here_with_pipe` (from StringExtensions)
15
+
16
+ These must be handled when converting String first.
17
+
18
+ ---
19
+
20
+ ### Task 1: Update gemspec, version, and remove Guard
21
+
22
+ **Files:**
23
+ - Modify: `midwire_common.gemspec`
24
+ - Modify: `lib/midwire_common/version.rb`
25
+ - Delete: `Guardfile`
26
+
27
+ **Step 1: Update gemspec**
28
+
29
+ Replace the full contents of `midwire_common.gemspec` with:
30
+
31
+ ```ruby
32
+ # coding: utf-8
33
+ require File.expand_path('../lib/midwire_common/version', __FILE__)
34
+
35
+ Gem::Specification.new do |spec|
36
+ spec.name = 'midwire_common'
37
+ spec.version = MidwireCommon::VERSION
38
+ spec.authors = ['Chris Blackburn']
39
+ spec.email = ['87a1779b@opayq.com']
40
+ spec.summary = 'Midwire Tech Ruby Library'
41
+ spec.description = 'A useful Ruby library'
42
+ spec.homepage = 'https://bitbucket.org/midwiretech/midwire_common'
43
+ spec.license = 'MIT'
44
+
45
+ spec.files = `git ls-files -z`.split("\x0")
46
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
47
+ spec.test_files = spec.files.grep(%r{^spec/})
48
+ spec.require_paths = ['lib']
49
+
50
+ spec.required_ruby_version = '>= 3.2'
51
+
52
+ spec.add_development_dependency 'debug', '~> 1.9'
53
+ spec.add_development_dependency 'rake', '~> 13.0'
54
+ spec.add_development_dependency 'rspec', '~> 3.12'
55
+ spec.add_development_dependency 'rubocop', '~> 1.60'
56
+ spec.add_development_dependency 'simplecov', '~> 0.22'
57
+ end
58
+ ```
59
+
60
+ **Step 2: Update version**
61
+
62
+ In `lib/midwire_common/version.rb`, change:
63
+
64
+ ```ruby
65
+ original_verbosity = $VERBOSE
66
+ $VERBOSE = nil
67
+ module MidwireCommon
68
+ VERSION = '2.0.0'.freeze
69
+ end
70
+ $VERBOSE = original_verbosity
71
+ ```
72
+
73
+ **Step 3: Delete Guardfile**
74
+
75
+ ```bash
76
+ git rm Guardfile
77
+ ```
78
+
79
+ **Step 4: Run bundle install**
80
+
81
+ ```bash
82
+ bundle install
83
+ ```
84
+
85
+ Expected: Success. Gemfile.lock updated with new dependency versions.
86
+
87
+ **Step 5: Commit**
88
+
89
+ ```bash
90
+ git add midwire_common.gemspec lib/midwire_common/version.rb Gemfile.lock
91
+ git commit -m "chore: update dependencies and bump to 2.0.0
92
+
93
+ - Ruby >= 3.2, RSpec 3.12, Rake 13, Rubocop 1.60
94
+ - Remove Guard, pry-nav
95
+ - Add debug gem"
96
+ ```
97
+
98
+ ---
99
+
100
+ ### Task 2: Update spec_helper
101
+
102
+ **Files:**
103
+ - Modify: `spec/spec_helper.rb`
104
+
105
+ **Step 1: Rewrite spec_helper**
106
+
107
+ Replace full contents of `spec/spec_helper.rb`:
108
+
109
+ ```ruby
110
+ if ENV['COVERAGE']
111
+ require 'simplecov'
112
+ SimpleCov.start do
113
+ add_filter 'spec/'
114
+ add_filter 'vendor/'
115
+ end
116
+ end
117
+
118
+ require 'debug'
119
+ require File.join(File.dirname(__FILE__), '..', 'lib', 'midwire_common')
120
+ require 'midwire_common/all'
121
+
122
+ PROJECT_ROOT = File.expand_path('..', File.dirname(__FILE__))
123
+
124
+ RSpec.configure do |config|
125
+ include MidwireCommon
126
+
127
+ config.expect_with :rspec do |expectations|
128
+ expectations.syntax = [:should, :expect]
129
+ end
130
+
131
+ config.mock_with :rspec do |mocks|
132
+ mocks.syntax = [:should, :expect]
133
+ end
134
+
135
+ config.order = 'random'
136
+
137
+ def capture(stream)
138
+ begin
139
+ stream = stream.to_s
140
+ eval "$#{stream} = StringIO.new"
141
+ yield
142
+ result = eval("$#{stream}").string
143
+ ensure
144
+ eval("$#{stream} = #{stream.upcase}")
145
+ end
146
+
147
+ result
148
+ end
149
+ end
150
+ ```
151
+
152
+ Key changes:
153
+ - `require 'pry'` → `require 'debug'`
154
+ - `config.color = true` removed (RSpec 3 defaults to color on TTY)
155
+ - Added `expectations.syntax = [:should, :expect]` to support both syntaxes during migration
156
+ - Added `mocks.syntax = [:should, :expect]` for same reason
157
+
158
+ **Step 2: Commit**
159
+
160
+ ```bash
161
+ git add spec/spec_helper.rb
162
+ git commit -m "chore: update spec_helper for RSpec 3 compatibility"
163
+ ```
164
+
165
+ Note: Tests will NOT pass at this point. That's expected — we haven't converted the source files yet.
166
+
167
+ ---
168
+
169
+ ### Task 3: Convert String to refinements
170
+
171
+ This is the first and most critical conversion because other modules depend on String methods.
172
+
173
+ **Files:**
174
+ - Modify: `lib/midwire_common/string.rb`
175
+ - Modify: `spec/lib/midwire_common/string_spec.rb`
176
+ - Modify: `spec/lib/midwire_common/array_spec.rb` (uses `here_with_pipe`)
177
+ - Modify: `spec/lib/midwire_common/time_spec.rb` (uses `numeric?`)
178
+ - Modify: `lib/tasks/version.rake` (uses `here_with_pipe`)
179
+
180
+ **Step 1: Rewrite string.rb with refinements**
181
+
182
+ Replace full contents of `lib/midwire_common/string.rb`:
183
+
184
+ ```ruby
185
+ module MidwireCommon
186
+ module StringExtensions
187
+ refine String.singleton_class do
188
+ def random(count = 6, ranges = [('a'..'z'), ('A'..'Z'), ('0'..'9')])
189
+ coll = ranges.map(&:to_a).flatten
190
+ (0..(count - 1)).map { coll[rand(coll.length)] }.join
191
+ end
192
+ end
193
+
194
+ refine String do
195
+ def left_substr(count)
196
+ slice(0, count)
197
+ end
198
+
199
+ def right_substr(count)
200
+ slice(-count, count)
201
+ end
202
+
203
+ # html = <<-stop.here_with_pipe(delimeter="\n")
204
+ # |<!-- Begin: comment -->
205
+ # |<script type="text/javascript">
206
+ # stop
207
+ def here_with_pipe(delimeter = ' ')
208
+ lines = split("\n")
209
+ lines.map! { |c| c.sub!(/\s*\|/, '') }
210
+ new_string = lines.join(delimeter)
211
+ replace(new_string)
212
+ end
213
+
214
+ def alpha_numeric?
215
+ regex = /^[a-zA-Z0-9]+$/
216
+ (self =~ regex) == 0 ? true : false
217
+ end
218
+
219
+ def email_address?
220
+ email_regex = /\A([\w+\-].?)+@[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z/i
221
+ (self =~ email_regex) == 0 ? true : false
222
+ end
223
+
224
+ def zipcode?
225
+ self =~ %r{^(\d{5})(-\d{4})?$}x ? true : false
226
+ end
227
+
228
+ def numeric?
229
+ Float(self)
230
+ rescue
231
+ false
232
+ else
233
+ true
234
+ end
235
+
236
+ def format_phone
237
+ gsub!(/[a-z,! \-\(\)\:\;\.\&\$]+/i, '')
238
+ '(' << slice(0..2) << ')' << slice(3..5) << '-' << slice(-4, 4)
239
+ end
240
+
241
+ def sanitize
242
+ gsub(/[^a-z0-9,! \-\(\)\:\;\.\&\$]+/i, '')
243
+ end
244
+
245
+ def sanitize!
246
+ gsub!(/[^a-z0-9,! \-\(\)\:\;\.\&\$]+/i, '')
247
+ end
248
+
249
+ def shorten(maxcount = 30)
250
+ if length >= maxcount
251
+ shortened = self[0, maxcount]
252
+ splitted = shortened.split(/\s/)
253
+ if splitted.length > 1
254
+ words = splitted.length
255
+ splitted[0, words - 1].join(' ') + '...'
256
+ else
257
+ shortened[0, maxcount - 3] + '...'
258
+ end
259
+ else
260
+ self
261
+ end
262
+ end
263
+
264
+ def escape_single_quotes
265
+ gsub(/[']/, '\\\\\'')
266
+ end
267
+
268
+ def escape_double_quotes
269
+ gsub(/["]/, '\\\\\"')
270
+ end
271
+
272
+ def snakerize
273
+ gsub(/::/, '/')
274
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
275
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
276
+ .tr('-', '_')
277
+ .downcase
278
+ end
279
+
280
+ def camelize
281
+ gsub(/\/(.?)/) { '::' + $1.upcase }.gsub(/(^|_)(.)/) { $2.upcase }
282
+ end
283
+ end
284
+ end
285
+ end
286
+ ```
287
+
288
+ **Step 2: Rewrite string_spec.rb**
289
+
290
+ Replace full contents of `spec/lib/midwire_common/string_spec.rb`:
291
+
292
+ ```ruby
293
+ require 'spec_helper'
294
+
295
+ using MidwireCommon::StringExtensions
296
+
297
+ RSpec.describe String do
298
+ it 'is a String' do
299
+ ''.should be_a String
300
+ end
301
+
302
+ it 'generates a random string' do
303
+ String.random.length.should == 6
304
+ end
305
+
306
+ context 'slicing methods' do
307
+ it "'left' returns the leftmost 'n' characters" do
308
+ 'My Bogus String'.left_substr(2).should == 'My'
309
+ end
310
+
311
+ it "'right' returns the rightmost 'n' characters " do
312
+ 'My Bogus String'.right_substr(2).should == 'ng'
313
+ end
314
+ end
315
+
316
+ context 'formatting and manipulation' do
317
+ it 'here_with_pipe - without linefeeds' do
318
+ html = <<-STOP.here_with_pipe
319
+ |<!-- Begin: comment -->
320
+ |<script type="text/javascript">
321
+ |</script>
322
+ STOP
323
+ html.should == '<!-- Begin: comment --> <script type="text/javascript"> </script>'
324
+ end
325
+
326
+ it 'here_with_pipe - with linefeeds' do
327
+ html = <<-STOP.here_with_pipe("\n")
328
+ |<!-- Begin: comment -->
329
+ |<script type="text/javascript">
330
+ |</script>
331
+ STOP
332
+ html.should == "<!-- Begin: comment -->\n<script type=\"text/javascript\">\n</script>"
333
+ end
334
+
335
+ it 'here_with_pipe - with no space delimeter' do
336
+ html = <<-STOP.here_with_pipe('')
337
+ |<!-- Begin: comment -->
338
+ |<script type="text/javascript">
339
+ |</script>
340
+ STOP
341
+ html.should == '<!-- Begin: comment --><script type="text/javascript"></script>'
342
+ end
343
+
344
+ it 'format_phone returns a formatted phone number string' do
345
+ expect('9132329999'.format_phone).to eq('(913)232-9999')
346
+ expect('913.232.9999'.format_phone).to eq('(913)232-9999')
347
+ expect('913 232 9999'.format_phone).to eq('(913)232-9999')
348
+ expect('913-232-9999'.format_phone).to eq('(913)232-9999')
349
+ end
350
+
351
+ it 'sanitizes itself' do
352
+ expect('|bogus|'.sanitize).to eq('bogus')
353
+ expect('|∫|ß'.sanitize).to eq('')
354
+ expect('ßogus'.sanitize).to eq('ogus')
355
+ expect('<tag>bogus</tag>'.sanitize).to eq('tagbogustag')
356
+ expect('<tag>.bogus.</tag>'.sanitize).to eq('tag.bogus.tag')
357
+ s = '|∫|ß'
358
+ s.sanitize!
359
+ s.should == ''
360
+ end
361
+
362
+ it 'shortens itself with elipses at the end' do
363
+ s = 'this is my very long string which I will eventually shorten with the enhanced String class that we are now testing.'
364
+ short = s.shorten
365
+ expect(short).to eq('this is my very long string...')
366
+ expect(short.length).to eq(30)
367
+
368
+ s = '1234567890123456789012345678901234567890'
369
+ short = s.shorten
370
+ expect(short).to eq('123456789012345678901234567...')
371
+ expect(short.length).to eq(30)
372
+
373
+ s = '12345678901234567890'
374
+ short = s.shorten
375
+ expect(short).to eq('12345678901234567890')
376
+ expect(short.length).to eq(20)
377
+ end
378
+
379
+ context 'quotes' do
380
+ it 'escapes single quotes' do
381
+ "this is a 'test'".escape_single_quotes.should == "this is a \\'test\\'"
382
+ end
383
+
384
+ it 'escapes double quotes' do
385
+ expect('this is a "test"'.escape_double_quotes)
386
+ .to eq('this is a \\\\"test\\\\"')
387
+ end
388
+ end
389
+ end
390
+
391
+ context 'characterization' do
392
+ it 'knows if it is alpha-numeric or not' do
393
+ 'abcd-9191'.alpha_numeric?.should eq(false)
394
+ 'abcd.9191'.alpha_numeric?.should eq(false)
395
+ 'abcd91910'.alpha_numeric?.should eq(true)
396
+ 'abcd_9191'.alpha_numeric?.should eq(false)
397
+ 'abcd 9191'.alpha_numeric?.should eq(false)
398
+ end
399
+
400
+ it 'knows if it is an email address or not' do
401
+ 'abcd_9191'.email_address?.should eq(false)
402
+ 'abcd@9191'.email_address?.should eq(false)
403
+ 'abcd@9191.info'.email_address?.should eq(true)
404
+ 'abcd-asdf@9191.com'.email_address?.should eq(true)
405
+ 'abcd_asdf@9191.com'.email_address?.should eq(true)
406
+ 'abcd.asdf@9191.com'.email_address?.should eq(true)
407
+ end
408
+
409
+ it 'knows if it is a zipcode or not' do
410
+ '13922-2356'.zipcode?.should eq(true)
411
+ '13922.2343'.zipcode?.should eq(false)
412
+ '13922 2342'.zipcode?.should eq(false)
413
+ 'ABSSD'.zipcode?.should eq(false)
414
+ 'i3323'.zipcode?.should eq(false)
415
+ '13922'.zipcode?.should eq(true)
416
+ end
417
+
418
+ it 'knows if it is numeric or not' do
419
+ '12341'.numeric?.should eq(true)
420
+ '12341.23'.numeric?.should eq(true)
421
+ '12341.00000000000000023'.numeric?.should eq(true)
422
+ '0.12341'.numeric?.should eq(true)
423
+ '0x2E'.numeric?.should eq(true)
424
+ ' 0.12341'.numeric?.should eq(true)
425
+ ' 0.12341 '.numeric?.should eq(true)
426
+ '.12341'.numeric?.should eq(true)
427
+ ' 12341.'.numeric?.should eq(false)
428
+ ' 12341. '.numeric?.should eq(false)
429
+ end
430
+ end
431
+
432
+ context '.snakerize' do
433
+ it 'changes CamelCased string to snake_cased' do
434
+ expect('CamelCased'.snakerize).to eq('camel_cased')
435
+ end
436
+
437
+ it 'handles doulbe-colon' do
438
+ expect('Camel::CasedTwo'.snakerize).to eq('camel/cased_two')
439
+ end
440
+ end
441
+
442
+ context '.camelize' do
443
+ it 'changes snake cased string to camelized' do
444
+ expect('camel_cased'.camelize).to eq('CamelCased')
445
+ end
446
+
447
+ it 'handles slash' do
448
+ expect('camel/cased_two'.camelize).to eq('Camel::CasedTwo')
449
+ end
450
+ end
451
+ end
452
+ ```
453
+
454
+ Changes: removed trim method specs, added `using MidwireCommon::StringExtensions` at top.
455
+
456
+ **Step 3: Add `using` to array_spec.rb (cross-dependency)**
457
+
458
+ Add `using MidwireCommon::StringExtensions` after the require line in `spec/lib/midwire_common/array_spec.rb` (for `here_with_pipe` usage on line 37).
459
+
460
+ **Step 4: Add `using` to time_spec.rb (cross-dependency)**
461
+
462
+ Add `using MidwireCommon::StringExtensions` after the require line in `spec/lib/midwire_common/time_spec.rb` (for `numeric?` usage).
463
+
464
+ **Step 5: Update version.rake**
465
+
466
+ At the top of `lib/tasks/version.rake`, after `require 'midwire_common/string'`, add:
467
+
468
+ ```ruby
469
+ using MidwireCommon::StringExtensions
470
+ ```
471
+
472
+ **Step 6: Run string specs**
473
+
474
+ ```bash
475
+ bundle exec rspec spec/lib/midwire_common/string_spec.rb
476
+ ```
477
+
478
+ Expected: All pass.
479
+
480
+ **Step 7: Commit**
481
+
482
+ ```bash
483
+ git add lib/midwire_common/string.rb spec/lib/midwire_common/string_spec.rb \
484
+ spec/lib/midwire_common/array_spec.rb spec/lib/midwire_common/time_spec.rb \
485
+ lib/tasks/version.rake
486
+ git commit -m "refactor: convert String monkey-patch to refinements
487
+
488
+ Remove trim/left_trim/right_trim (use stdlib strip/lstrip/rstrip).
489
+ Add 'using' to dependent specs and version.rake."
490
+ ```
491
+
492
+ ---
493
+
494
+ ### Task 4: Convert Array to refinements
495
+
496
+ **Files:**
497
+ - Modify: `lib/midwire_common/array.rb`
498
+ - Modify: `spec/lib/midwire_common/array_spec.rb`
499
+
500
+ **Step 1: Rewrite array.rb**
501
+
502
+ Replace full contents of `lib/midwire_common/array.rb`:
503
+
504
+ ```ruby
505
+ module MidwireCommon
506
+ module ArrayExtensions
507
+ refine Array do
508
+ def count_occurrences
509
+ hash = Hash.new(0)
510
+ each { |elem| hash[elem] += 1 }
511
+ hash
512
+ end
513
+
514
+ def randomize
515
+ sort_by { rand }
516
+ end
517
+
518
+ def randomize!
519
+ replace(randomize)
520
+ end
521
+
522
+ def sort_case_insensitive
523
+ sort_by(&:downcase)
524
+ end
525
+
526
+ def each_with_first_last(first_code, main_code, last_code)
527
+ each_with_index do |item, ndx|
528
+ case ndx
529
+ when 0 then first_code.call(item)
530
+ when size - 1 then last_code.call(item)
531
+ else main_code.call(item)
532
+ end
533
+ end
534
+ end
535
+
536
+ # Make a string from a multi-dimensional array:
537
+ # 1 dimension:
538
+ # [1,2,3].superjoin(["pre","between","post"])
539
+ #
540
+ # 2 dimensions (html table):
541
+ # [[1,2],[2,3]].superjoin(%w{<table><tr> </tr><tr> </tr></table>}, %w{<td> </td><td> </td>})
542
+ def superjoin(*ldescr)
543
+ dim = ldescr[0]
544
+ rest = ldescr[1..]
545
+ dim[0] + map do |arr|
546
+ (arr.respond_to?(:superjoin) && !rest.empty?) ? arr.superjoin(*rest) : arr.to_s
547
+ end.join(dim[1]) + dim[2]
548
+ end
549
+ end
550
+ end
551
+ end
552
+ ```
553
+
554
+ Changes: removed `binsearch`, wrapped in refinement module, `ldescr[1..-1]` → `ldescr[1..]`.
555
+
556
+ **Step 2: Rewrite array_spec.rb**
557
+
558
+ Replace full contents of `spec/lib/midwire_common/array_spec.rb`:
559
+
560
+ ```ruby
561
+ require 'spec_helper'
562
+
563
+ using MidwireCommon::StringExtensions
564
+ using MidwireCommon::ArrayExtensions
565
+
566
+ RSpec.describe Array do
567
+ it 'counts occurrences of an element' do
568
+ [1, 2, 2, 3, 3, 3].count_occurrences.should eq(1 => 1, 2 => 2, 3 => 3)
569
+ %w(asdf asdf qwer asdf).count_occurrences.should == {
570
+ 'asdf' => 3,
571
+ 'qwer' => 1
572
+ }
573
+ end
574
+
575
+ it 'randomizes the order of an array' do
576
+ myarray = [1, 2, 3, 4, 5, 6, 7, 8, 9, '0']
577
+ myarray.randomize.should_not eq(myarray)
578
+ myarray.randomize!
579
+ myarray.should_not == [1, 2, 3, 4, 5, 6, 7, 8, 9, '0']
580
+ end
581
+
582
+ it 'sorts elements case insensitive' do
583
+ myarray = %w(zebra ghost Zebra cat Cat)
584
+ myarray.sort_case_insensitive.should eq(%w(Cat cat ghost Zebra zebra))
585
+ end
586
+
587
+ it 'can process first and last entries differently than others' do
588
+ text = ''
589
+ ['KU', 'K-State', 'MU'].each_with_first_last(
590
+ lambda do |team|
591
+ text += "#{team} came first in the NCAA basketball tournament.\n"
592
+ end,
593
+ lambda do |team|
594
+ text += "#{team} did not come first or last in the final four.\n"
595
+ end,
596
+ lambda do |team|
597
+ text += "#{team} came last in the final four this year.\n"
598
+ end
599
+ )
600
+ text.should == <<-string.here_with_pipe("\n")
601
+ |KU came first in the NCAA basketball tournament.
602
+ |K-State did not come first or last in the final four.
603
+ |MU came last in the final four this year.
604
+ |
605
+ string
606
+ end
607
+
608
+ it 'can superjoin elements' do
609
+ [1, 2, 3].superjoin(['->', '+', '<-']).should eq('->1+2+3<-')
610
+ [[1, 2], [2, 3]].superjoin(
611
+ %w(<table><tr> </tr><tr> </tr></table>), %w(<td> </td><td> </td>)
612
+ ).should eq(
613
+ '<table><tr><td>1</td><td>2</td></tr><tr><td>2</td><td>3</td></tr></table>'
614
+ )
615
+ end
616
+ end
617
+ ```
618
+
619
+ Changes: removed `binsearch` test, added `using` for both String and Array extensions.
620
+
621
+ **Step 3: Run array specs**
622
+
623
+ ```bash
624
+ bundle exec rspec spec/lib/midwire_common/array_spec.rb
625
+ ```
626
+
627
+ Expected: All pass.
628
+
629
+ **Step 4: Commit**
630
+
631
+ ```bash
632
+ git add lib/midwire_common/array.rb spec/lib/midwire_common/array_spec.rb
633
+ git commit -m "refactor: convert Array monkey-patch to refinements
634
+
635
+ Remove binsearch (use stdlib Array#bsearch)."
636
+ ```
637
+
638
+ ---
639
+
640
+ ### Task 5: Convert Hash to refinements and namespace BottomlessHash
641
+
642
+ **Files:**
643
+ - Modify: `lib/midwire_common/hash.rb`
644
+ - Modify: `spec/lib/midwire_common/hash_spec.rb`
645
+
646
+ **Step 1: Rewrite hash.rb**
647
+
648
+ Replace full contents of `lib/midwire_common/hash.rb`:
649
+
650
+ ```ruby
651
+ require 'uri'
652
+
653
+ module MidwireCommon
654
+ # By Nick Ostrovsky
655
+ # http://firedev.com/posts/2015/bottomless-ruby-hash/
656
+ class BottomlessHash < Hash
657
+ def initialize
658
+ super(&-> (hash, key) { hash[key] = self.class.new })
659
+ end
660
+
661
+ def self.from_hash(hash)
662
+ new.merge(hash)
663
+ end
664
+ end
665
+
666
+ module HashExtensions
667
+ refine Hash do
668
+ def bottomless
669
+ MidwireCommon::BottomlessHash.from_hash(self)
670
+ end
671
+
672
+ def grep(pattern)
673
+ each_with_object([]) do |kv, res|
674
+ res << kv if kv[0] =~ pattern || kv[1] =~ pattern
675
+ res
676
+ end
677
+ end
678
+
679
+ def diff(other)
680
+ (keys + other.keys).uniq.each_with_object({}) do |key, memo|
681
+ unless self[key] == other[key]
682
+ memo[key] = if self[key].is_a?(Hash) && other[key].is_a?(Hash)
683
+ self[key].diff(other[key])
684
+ else
685
+ [self[key], other[key]]
686
+ end
687
+ end
688
+ memo
689
+ end
690
+ end
691
+
692
+ def apply_diff!(changes, direction = :right)
693
+ path = [[self, changes]]
694
+ pos, local_changes = path.pop
695
+ while local_changes
696
+ local_changes.each_pair do |key, change|
697
+ if change.is_a?(Array)
698
+ pos[key] = (direction == :right) ? change[1] : change[0]
699
+ else
700
+ path.push([pos[key], change])
701
+ end
702
+ end
703
+ pos, local_changes = path.pop
704
+ end
705
+ self
706
+ end
707
+
708
+ def apply_diff(changes, direction = :right)
709
+ cloned = clone
710
+ path = [[cloned, changes]]
711
+ pos, local_changes = path.pop
712
+ while local_changes
713
+ local_changes.each_pair do |key, change|
714
+ if change.is_a?(Array)
715
+ pos[key] = (direction == :right) ? change[1] : change[0]
716
+ else
717
+ pos[key] = pos[key].clone
718
+ path.push([pos[key], change])
719
+ end
720
+ end
721
+ pos, local_changes = path.pop
722
+ end
723
+ cloned
724
+ end
725
+
726
+ # Usage { a: 1, b: 2, c: 3 }.only(:a) -> { a: 1 }
727
+ def only(*keys)
728
+ dup.reject { |key, _v| !keys.flatten.include?(key.to_sym) }
729
+ end
730
+
731
+ # Usage h = { a: 1, b: 2, c: 3 }.pop(:a) -> { a: 1 }
732
+ # ... and now h == { b: 2, c: 3 }
733
+ def pop(*keys)
734
+ ret = reject { |key, _v| !keys.flatten.include?(key.to_sym) }
735
+ reject! { |key, _v| keys.flatten.include?(key.to_sym) }
736
+ ret
737
+ end
738
+
739
+ # Usage { a: 1, b: 2, c: 3 }.to_query_string #=> "a=1&b=2&c=3"
740
+ def to_query_string
741
+ URI.encode_www_form(self)
742
+ end
743
+
744
+ def symbolize_keys!
745
+ keys.each do |key|
746
+ self[(key.to_sym rescue key) || key] = delete(key)
747
+ end
748
+ self
749
+ end
750
+
751
+ def symbolize_keys
752
+ dup.symbolize_keys!
753
+ end
754
+
755
+ def recursively_symbolize_keys!
756
+ symbolize_keys!
757
+ values.each do |value|
758
+ value.recursively_symbolize_keys! if value.is_a?(Hash)
759
+ end
760
+ self
761
+ end
762
+ end
763
+ end
764
+ end
765
+ ```
766
+
767
+ Changes: removed `except` (built into Ruby 3.0+), replaced `URI.encode` with `URI.encode_www_form`, namespaced `BottomlessHash` under `MidwireCommon`, wrapped in refinement module.
768
+
769
+ **Step 2: Rewrite hash_spec.rb**
770
+
771
+ Replace full contents of `spec/lib/midwire_common/hash_spec.rb`:
772
+
773
+ ```ruby
774
+ require 'spec_helper'
775
+
776
+ using MidwireCommon::HashExtensions
777
+
778
+ RSpec.describe MidwireCommon::BottomlessHash do
779
+ subject { described_class.new }
780
+
781
+ it 'does not raise on missing key' do
782
+ expect do
783
+ subject[:missing][:key]
784
+ end.to_not raise_error
785
+ end
786
+
787
+ it 'returns an empty value on missing key' do
788
+ expect(subject[:missing][:key]).to be_empty
789
+ end
790
+
791
+ it 'stores and returns keys' do
792
+ subject[:existing][:key] = :value
793
+ expect(subject[:existing][:key]).to eq(:value)
794
+ end
795
+
796
+ context '#from_hash' do
797
+ let(:hash) do
798
+ { existing: { key: { value: :hello } } }
799
+ end
800
+
801
+ subject do
802
+ described_class.from_hash(hash)
803
+ end
804
+
805
+ it 'returns old hash values' do
806
+ expect(subject[:existing][:key][:value]).to eq(:hello)
807
+ end
808
+
809
+ it 'provides a bottomless version' do
810
+ expect(subject[:missing][:key]).to be_empty
811
+ end
812
+
813
+ it 'stores and returns new values' do
814
+ subject[:existing][:key] = :value
815
+ expect(subject[:existing][:key]).to eq(:value)
816
+ end
817
+
818
+ it 'converts nested hashes as well' do
819
+ expect do
820
+ subject[:existing][:key][:missing]
821
+ end.to_not raise_error
822
+ end
823
+ end
824
+ end
825
+
826
+ RSpec.describe Hash do
827
+ it 'greps key/value pairs using a regular expression' do
828
+ h = { a: 'this is a test', 'b' => 'this is not the answer' }
829
+ expect(h.grep(/a test/)).to eq([[:a, 'this is a test']])
830
+ expect(h.grep(/b/)).to eq([['b', 'this is not the answer']])
831
+ expect(
832
+ h.grep(/^this/)
833
+ ).to eq([[:a, 'this is a test'], ['b', 'this is not the answer']])
834
+ end
835
+
836
+ it 'returns elements for discretely passed keys' do
837
+ expect({ a: 1, b: 2, c: 3 }.only(:a)).to eq(a: 1)
838
+ end
839
+
840
+ it 'pops an element off of the stack' do
841
+ h = { a: 1, b: 2, c: 3 }
842
+ expect(h.pop(:a)).to eq(a: 1)
843
+ expect(h).to eq(b: 2, c: 3)
844
+ end
845
+
846
+ it 'returns a query string' do
847
+ expect({ a: 1, b: 2, c: 3 }.to_query_string).to eq('a=1&b=2&c=3')
848
+ end
849
+
850
+ it 'symbolizes its keys' do
851
+ h = { 'a' => 1, 'b' => 2, 'c' => 3 }
852
+ expect(h.symbolize_keys).to eq(a: 1, b: 2, c: 3)
853
+ h.symbolize_keys!
854
+ expect(h).to eq(a: 1, b: 2, c: 3)
855
+ end
856
+
857
+ it 'recursively symbolizes its keys' do
858
+ h = {
859
+ 'a' => 1,
860
+ 'b' => {
861
+ 'a' => 1,
862
+ 'b' => 2,
863
+ 'c' => {
864
+ 'a' => 1,
865
+ 'b' => 2,
866
+ 'c' => 3
867
+ }
868
+ },
869
+ 'c' => 3
870
+ }
871
+ expect(h.recursively_symbolize_keys!).to eq(
872
+ a: 1, b: { a: 1, b: 2, c: { a: 1, b: 2, c: 3 } }, c: 3
873
+ )
874
+ end
875
+
876
+ context 'diff methods' do
877
+ let(:h1_keys) { { 'a' => 1, 'b' => 2, 'c' => 3 } }
878
+ let(:h2_keys) { { 'a' => 1, 'b' => 2, 'd' => 3 } }
879
+
880
+ let(:h1_keys_nested) { { 'a' => 1, 'b' => 2, { 'x' => 99 } => 3 } }
881
+ let(:h2_keys_nested) { { 'a' => 1, 'b' => 2, { 'x' => 99 } => 4 } }
882
+
883
+ let(:h1_values) { { 'a' => 1, 'b' => 2, 'c' => 3 } }
884
+ let(:h2_values) { { 'a' => 1, 'b' => 2, 'c' => 4 } }
885
+
886
+ context '.diff' do
887
+ it 'reports different keys' do
888
+ expect(h1_keys.diff(h2_keys)).to eq('c' => [3, nil], 'd' => [nil, 3])
889
+ end
890
+
891
+ it 'reports different values' do
892
+ expect(h1_values.diff(h2_values)).to eq('c' => [3, 4])
893
+ end
894
+
895
+ it 'reports different keys even with nested hashes' do
896
+ expect(h1_keys_nested.diff(h2_keys_nested)).to eq(
897
+ { 'x' => 99 } => [3, 4]
898
+ )
899
+ end
900
+ end
901
+
902
+ context '.apply_diff!' do
903
+ it 'applies diff to the right by default' do
904
+ diff = h1_values.diff(h2_values)
905
+ result = h1_values.apply_diff!(diff)
906
+ expect(result).to eq('a' => 1, 'b' => 2, 'c' => 4)
907
+ expect(h1_values).to eq('a' => 1, 'b' => 2, 'c' => 4)
908
+ end
909
+
910
+ it 'applies diff to the left' do
911
+ diff = h1_values.diff(h2_values)
912
+ result = h2_values.apply_diff!(diff, :left)
913
+ expect(result).to eq('a' => 1, 'b' => 2, 'c' => 3)
914
+ end
915
+ end
916
+
917
+ context '.apply_diff' do
918
+ it 'returns a new hash with diff applied' do
919
+ diff = h1_values.diff(h2_values)
920
+ result = h1_values.apply_diff(diff)
921
+ expect(result).to eq('a' => 1, 'b' => 2, 'c' => 4)
922
+ expect(h1_values).to eq('a' => 1, 'b' => 2, 'c' => 3)
923
+ end
924
+
925
+ it 'applies diff to the left' do
926
+ diff = h1_values.diff(h2_values)
927
+ result = h2_values.apply_diff(diff, :left)
928
+ expect(result).to eq('a' => 1, 'b' => 2, 'c' => 3)
929
+ expect(h2_values).to eq('a' => 1, 'b' => 2, 'c' => 4)
930
+ end
931
+ end
932
+ end
933
+ end
934
+ ```
935
+
936
+ Changes: removed `except` test, added `apply_diff` and `apply_diff!` specs, namespaced `BottomlessHash` reference, added `using`.
937
+
938
+ **Step 3: Run hash specs**
939
+
940
+ ```bash
941
+ bundle exec rspec spec/lib/midwire_common/hash_spec.rb
942
+ ```
943
+
944
+ Expected: All pass.
945
+
946
+ **Step 4: Commit**
947
+
948
+ ```bash
949
+ git add lib/midwire_common/hash.rb spec/lib/midwire_common/hash_spec.rb
950
+ git commit -m "refactor: convert Hash monkey-patch to refinements
951
+
952
+ Remove Hash#except (built into Ruby 3.0+).
953
+ Replace URI.encode with URI.encode_www_form.
954
+ Namespace BottomlessHash under MidwireCommon.
955
+ Add specs for apply_diff and apply_diff!."
956
+ ```
957
+
958
+ ---
959
+
960
+ ### Task 6: Convert Integer (replacing Fixnum) to refinements
961
+
962
+ **Files:**
963
+ - Modify: `lib/midwire_common/fixnum.rb` → becomes `lib/midwire_common/integer.rb`
964
+ - Modify: `spec/lib/midwire_common/fixnum_spec.rb` → becomes `spec/lib/midwire_common/integer_spec.rb`
965
+
966
+ **Step 1: Create integer.rb**
967
+
968
+ Create `lib/midwire_common/integer.rb`:
969
+
970
+ ```ruby
971
+ require 'midwire_common'
972
+
973
+ module MidwireCommon
974
+ module IntegerExtensions
975
+ refine Integer do
976
+ include MidwireCommon::NumberBehavior
977
+ end
978
+ end
979
+ end
980
+ ```
981
+
982
+ **Step 2: Delete fixnum.rb**
983
+
984
+ ```bash
985
+ git rm lib/midwire_common/fixnum.rb
986
+ ```
987
+
988
+ **Step 3: Create integer_spec.rb**
989
+
990
+ Create `spec/lib/midwire_common/integer_spec.rb`:
991
+
992
+ ```ruby
993
+ require 'spec_helper'
994
+
995
+ using MidwireCommon::IntegerExtensions
996
+
997
+ RSpec.describe Integer do
998
+ it 'can format itself with commas' do
999
+ 8_729_928_827.commify.should == '8,729,928,827'
1000
+ end
1001
+ end
1002
+ ```
1003
+
1004
+ Note: removed `odd?`/`even?` tests — those are built into Integer.
1005
+
1006
+ **Step 4: Delete fixnum_spec.rb**
1007
+
1008
+ ```bash
1009
+ git rm spec/lib/midwire_common/fixnum_spec.rb
1010
+ ```
1011
+
1012
+ **Step 5: Run integer specs**
1013
+
1014
+ ```bash
1015
+ bundle exec rspec spec/lib/midwire_common/integer_spec.rb
1016
+ ```
1017
+
1018
+ Expected: All pass.
1019
+
1020
+ **Step 6: Commit**
1021
+
1022
+ ```bash
1023
+ git add lib/midwire_common/integer.rb spec/lib/midwire_common/integer_spec.rb
1024
+ git commit -m "refactor: replace Fixnum with Integer refinements
1025
+
1026
+ Fixnum was unified into Integer in Ruby 2.4.
1027
+ Remove odd?/even? tests (built-in)."
1028
+ ```
1029
+
1030
+ ---
1031
+
1032
+ ### Task 7: Convert Float to refinements
1033
+
1034
+ **Files:**
1035
+ - Modify: `lib/midwire_common/float.rb`
1036
+ - Modify: `spec/lib/midwire_common/float_spec.rb`
1037
+
1038
+ **Step 1: Rewrite float.rb**
1039
+
1040
+ Replace full contents of `lib/midwire_common/float.rb`:
1041
+
1042
+ ```ruby
1043
+ require 'midwire_common'
1044
+
1045
+ module MidwireCommon
1046
+ module FloatExtensions
1047
+ refine Float do
1048
+ include MidwireCommon::NumberBehavior
1049
+ end
1050
+ end
1051
+ end
1052
+ ```
1053
+
1054
+ **Step 2: Rewrite float_spec.rb**
1055
+
1056
+ Replace full contents of `spec/lib/midwire_common/float_spec.rb`:
1057
+
1058
+ ```ruby
1059
+ require 'spec_helper'
1060
+
1061
+ using MidwireCommon::FloatExtensions
1062
+
1063
+ RSpec.describe Float do
1064
+ it 'can format itself with commas' do
1065
+ 8_729_928_827.0.commify.should eq('8,729,928,827.0')
1066
+ 8_729_928_827.20332002.commify.should eq('8,729,928,827.20332')
1067
+ end
1068
+ end
1069
+ ```
1070
+
1071
+ **Step 3: Run float specs**
1072
+
1073
+ ```bash
1074
+ bundle exec rspec spec/lib/midwire_common/float_spec.rb
1075
+ ```
1076
+
1077
+ Expected: All pass.
1078
+
1079
+ **Step 4: Commit**
1080
+
1081
+ ```bash
1082
+ git add lib/midwire_common/float.rb spec/lib/midwire_common/float_spec.rb
1083
+ git commit -m "refactor: convert Float monkey-patch to refinements"
1084
+ ```
1085
+
1086
+ ---
1087
+
1088
+ ### Task 8: Convert Enumerable to refinements
1089
+
1090
+ **Files:**
1091
+ - Modify: `lib/midwire_common/enumerable.rb`
1092
+ - Modify: `spec/lib/midwire_common/enumerable_spec.rb`
1093
+
1094
+ **Step 1: Rewrite enumerable.rb**
1095
+
1096
+ Replace full contents of `lib/midwire_common/enumerable.rb`:
1097
+
1098
+ ```ruby
1099
+ module MidwireCommon
1100
+ module EnumerableExtensions
1101
+ refine Enumerable do
1102
+ def sort_by_frequency
1103
+ histogram = each_with_object(Hash.new(0)) do |elem, hash|
1104
+ hash[elem] += 1
1105
+ hash
1106
+ end
1107
+ sort_by { |elem| [histogram[elem] * -1, elem] }
1108
+ end
1109
+ end
1110
+ end
1111
+ end
1112
+ ```
1113
+
1114
+ **Step 2: Rewrite enumerable_spec.rb**
1115
+
1116
+ Replace full contents of `spec/lib/midwire_common/enumerable_spec.rb`:
1117
+
1118
+ ```ruby
1119
+ require 'spec_helper'
1120
+
1121
+ using MidwireCommon::EnumerableExtensions
1122
+
1123
+ RSpec.describe Enumerable do
1124
+ it 'can sort by frequency of occurrences' do
1125
+ [1, 2, 3, 3, 3, 3, 2].sort_by_frequency.should eq([3, 3, 3, 3, 2, 2, 1])
1126
+ %w(a b c d e f a f f b f a).sort_by_frequency
1127
+ .should eq(%w(f f f f a a a b b c d e))
1128
+ end
1129
+ end
1130
+ ```
1131
+
1132
+ **Step 3: Run enumerable specs**
1133
+
1134
+ ```bash
1135
+ bundle exec rspec spec/lib/midwire_common/enumerable_spec.rb
1136
+ ```
1137
+
1138
+ Expected: All pass.
1139
+
1140
+ **Step 4: Commit**
1141
+
1142
+ ```bash
1143
+ git add lib/midwire_common/enumerable.rb spec/lib/midwire_common/enumerable_spec.rb
1144
+ git commit -m "refactor: convert Enumerable monkey-patch to refinements"
1145
+ ```
1146
+
1147
+ ---
1148
+
1149
+ ### Task 9: Convert Time to refinements
1150
+
1151
+ **Files:**
1152
+ - Modify: `lib/midwire_common/time.rb`
1153
+ - Modify: `spec/lib/midwire_common/time_spec.rb`
1154
+
1155
+ **Step 1: Rewrite time.rb**
1156
+
1157
+ Replace full contents of `lib/midwire_common/time.rb`:
1158
+
1159
+ ```ruby
1160
+ module MidwireCommon
1161
+ module TimeExtensions
1162
+ refine Time.singleton_class do
1163
+ def timestamp(resolution = 3)
1164
+ return Time.now.strftime('%Y%m%d%H%M%S') if resolution < 1
1165
+ Time.now.strftime("%Y%m%d%H%M%S.%#{resolution}N")
1166
+ end
1167
+ end
1168
+ end
1169
+ end
1170
+ ```
1171
+
1172
+ **Step 2: Rewrite time_spec.rb**
1173
+
1174
+ Replace full contents of `spec/lib/midwire_common/time_spec.rb`:
1175
+
1176
+ ```ruby
1177
+ require 'spec_helper'
1178
+
1179
+ using MidwireCommon::StringExtensions
1180
+ using MidwireCommon::TimeExtensions
1181
+
1182
+ RSpec.describe Time do
1183
+ context 'generates a timestamp' do
1184
+ it 'appropriate for a filename' do
1185
+ ts = Time.timestamp
1186
+ expect(ts.length).to eq(18)
1187
+ expect(ts.numeric?).to eq(true)
1188
+ end
1189
+
1190
+ it 'with the only seconds' do
1191
+ ts = Time.timestamp(0)
1192
+ expect(ts.length).to eq(14)
1193
+ expect(ts.numeric?).to eq(true)
1194
+ end
1195
+
1196
+ it 'defaults to nanosecond resolution' do
1197
+ ts = Time.timestamp
1198
+ expect(ts.length).to eq(18)
1199
+ expect(ts.numeric?).to eq(true)
1200
+ end
1201
+
1202
+ it 'with the millisecond resolution' do
1203
+ ts = Time.timestamp(3)
1204
+ expect(ts.length).to eq(18)
1205
+ expect(ts.numeric?).to eq(true)
1206
+ end
1207
+
1208
+ it 'with the microsecond resolution' do
1209
+ ts = Time.timestamp(6)
1210
+ expect(ts.length).to eq(21)
1211
+ expect(ts.numeric?).to eq(true)
1212
+ end
1213
+
1214
+ it 'with the nanosecond resolution' do
1215
+ ts = Time.timestamp(9)
1216
+ expect(ts.length).to eq(24)
1217
+ expect(ts.numeric?).to eq(true)
1218
+ end
1219
+
1220
+ it 'with the picosecond resolution' do
1221
+ ts = Time.timestamp(12)
1222
+ expect(ts.length).to eq(27)
1223
+ expect(ts.numeric?).to eq(true)
1224
+ end
1225
+ end
1226
+ end
1227
+ ```
1228
+
1229
+ Note: needs `using MidwireCommon::StringExtensions` because tests call `numeric?` on the timestamp strings.
1230
+
1231
+ **Step 3: Run time specs**
1232
+
1233
+ ```bash
1234
+ bundle exec rspec spec/lib/midwire_common/time_spec.rb
1235
+ ```
1236
+
1237
+ Expected: All pass.
1238
+
1239
+ **Step 4: Commit**
1240
+
1241
+ ```bash
1242
+ git add lib/midwire_common/time.rb spec/lib/midwire_common/time_spec.rb
1243
+ git commit -m "refactor: convert Time monkey-patch to refinements"
1244
+ ```
1245
+
1246
+ ---
1247
+
1248
+ ### Task 10: Convert File to refinements
1249
+
1250
+ **Files:**
1251
+ - Modify: `lib/midwire_common/file.rb`
1252
+ - Delete: `lib/midwire_common/file/stat.rb` (merge into file.rb)
1253
+ - Modify: `spec/lib/midwire_common/file/stat_spec.rb`
1254
+
1255
+ **Step 1: Rewrite file.rb (consolidating file/stat.rb)**
1256
+
1257
+ Replace full contents of `lib/midwire_common/file.rb`:
1258
+
1259
+ ```ruby
1260
+ module MidwireCommon
1261
+ module FileExtensions
1262
+ refine File::Stat.singleton_class do
1263
+ def device_name(file)
1264
+ Dir['/dev/*'].inject({}) do |hash, node|
1265
+ hash.update(File.stat(node).rdev => node)
1266
+ end.values_at(File.stat(file).dev).first || nil
1267
+ end
1268
+ end
1269
+ end
1270
+ end
1271
+ ```
1272
+
1273
+ **Step 2: Delete file/stat.rb**
1274
+
1275
+ ```bash
1276
+ git rm lib/midwire_common/file/stat.rb
1277
+ rmdir lib/midwire_common/file 2>/dev/null || true
1278
+ ```
1279
+
1280
+ **Step 3: Rewrite stat_spec.rb**
1281
+
1282
+ Replace full contents of `spec/lib/midwire_common/file/stat_spec.rb`:
1283
+
1284
+ ```ruby
1285
+ require 'spec_helper'
1286
+
1287
+ using MidwireCommon::FileExtensions
1288
+
1289
+ RSpec.describe File::Stat do
1290
+ it 'knows on which device it resides' do
1291
+ dev = File::Stat.device_name('/tmp')
1292
+ dev.should_not be_nil
1293
+ end
1294
+ end
1295
+ ```
1296
+
1297
+ **Step 4: Run file specs**
1298
+
1299
+ ```bash
1300
+ bundle exec rspec spec/lib/midwire_common/file/stat_spec.rb
1301
+ ```
1302
+
1303
+ Expected: All pass.
1304
+
1305
+ **Step 5: Commit**
1306
+
1307
+ ```bash
1308
+ git add lib/midwire_common/file.rb spec/lib/midwire_common/file/stat_spec.rb
1309
+ git commit -m "refactor: convert File::Stat monkey-patch to refinements
1310
+
1311
+ Consolidate file/stat.rb into file.rb."
1312
+ ```
1313
+
1314
+ ---
1315
+
1316
+ ### Task 11: Update all.rb and midwire_common.rb
1317
+
1318
+ **Files:**
1319
+ - Modify: `lib/midwire_common/all.rb`
1320
+ - Modify: `lib/midwire_common.rb`
1321
+
1322
+ **Step 1: Rewrite all.rb**
1323
+
1324
+ Replace full contents of `lib/midwire_common/all.rb`:
1325
+
1326
+ ```ruby
1327
+ require 'midwire_common/array'
1328
+ require 'midwire_common/enumerable'
1329
+ require 'midwire_common/data_file_cache'
1330
+ require 'midwire_common/file'
1331
+ require 'midwire_common/integer'
1332
+ require 'midwire_common/float'
1333
+ require 'midwire_common/hash'
1334
+ require 'midwire_common/string'
1335
+ require 'midwire_common/time'
1336
+ require 'midwire_common/time_tool'
1337
+ require 'midwire_common/yaml_setting'
1338
+
1339
+ module MidwireCommon
1340
+ module All
1341
+ include MidwireCommon::StringExtensions
1342
+ include MidwireCommon::ArrayExtensions
1343
+ include MidwireCommon::HashExtensions
1344
+ include MidwireCommon::IntegerExtensions
1345
+ include MidwireCommon::FloatExtensions
1346
+ include MidwireCommon::EnumerableExtensions
1347
+ include MidwireCommon::TimeExtensions
1348
+ include MidwireCommon::FileExtensions
1349
+ end
1350
+ end
1351
+ ```
1352
+
1353
+ Key change: `fixnum` → `integer`. Adds `MidwireCommon::All` module composing all refinement modules so consumers can `using MidwireCommon::All`.
1354
+
1355
+ **Step 2: Update midwire_common.rb**
1356
+
1357
+ Replace full contents of `lib/midwire_common.rb`:
1358
+
1359
+ ```ruby
1360
+ require 'pathname'
1361
+ require 'midwire_common/version'
1362
+
1363
+ module MidwireCommon
1364
+ class << self
1365
+ def root
1366
+ Pathname.new(File.dirname(__FILE__)).parent
1367
+ end
1368
+ end
1369
+
1370
+ autoload :BottomlessHash, 'midwire_common/hash'
1371
+ autoload :DataFileCache, 'midwire_common/data_file_cache'
1372
+ autoload :NumberBehavior, 'midwire_common/number_behavior'
1373
+ autoload :RakeHelper, 'midwire_common/rake_helper'
1374
+ autoload :TimeTool, 'midwire_common/time_tool'
1375
+ autoload :YamlSetting, 'midwire_common/yaml_setting'
1376
+ end
1377
+ ```
1378
+
1379
+ Added autoloads for all utility classes so they're accessible without requiring `all.rb`.
1380
+
1381
+ **Step 3: Run full test suite**
1382
+
1383
+ ```bash
1384
+ bundle exec rspec
1385
+ ```
1386
+
1387
+ Expected: All tests pass.
1388
+
1389
+ **Step 4: Commit**
1390
+
1391
+ ```bash
1392
+ git add lib/midwire_common/all.rb lib/midwire_common.rb
1393
+ git commit -m "refactor: update all.rb with composed refinements module
1394
+
1395
+ Add MidwireCommon::All for 'using MidwireCommon::All'.
1396
+ Add autoloads for utility classes."
1397
+ ```
1398
+
1399
+ ---
1400
+
1401
+ ### Task 12: Update README and CLAUDE.md
1402
+
1403
+ **Files:**
1404
+ - Modify: `README.md`
1405
+ - Modify: `CLAUDE.md`
1406
+
1407
+ **Step 1: Update README.md**
1408
+
1409
+ Update the version string and usage section in `README.md` to reflect the new refinements API:
1410
+
1411
+ - Change `**Version: 1.1.1**` to `**Version: 2.0.0**`
1412
+ - Update the Usage section to show `using` instead of `require`:
1413
+
1414
+ ```markdown
1415
+ ## Usage
1416
+
1417
+ ### Ruby Class Extensions
1418
+
1419
+ Require the extensions, then activate them with `using`:
1420
+
1421
+ require 'midwire_common/all'
1422
+ using MidwireCommon::All
1423
+
1424
+ ... or include individual refinement modules:
1425
+
1426
+ require 'midwire_common/string'
1427
+ using MidwireCommon::StringExtensions
1428
+ ```
1429
+
1430
+ - Update the `required_ruby_version` note if present
1431
+
1432
+ **Step 2: Update CLAUDE.md**
1433
+
1434
+ Update loading section to document the `using` pattern and the `MidwireCommon::All` module.
1435
+
1436
+ **Step 3: Commit**
1437
+
1438
+ ```bash
1439
+ git add README.md CLAUDE.md
1440
+ git commit -m "docs: update README and CLAUDE.md for 2.0.0 refinements API"
1441
+ ```
1442
+
1443
+ ---
1444
+
1445
+ ### Task 13: Final verification
1446
+
1447
+ **Step 1: Run full test suite**
1448
+
1449
+ ```bash
1450
+ bundle exec rspec
1451
+ ```
1452
+
1453
+ Expected: All tests pass, zero failures.
1454
+
1455
+ **Step 2: Run tests with coverage**
1456
+
1457
+ ```bash
1458
+ COVERAGE=1 bundle exec rspec
1459
+ ```
1460
+
1461
+ Review the coverage report for any gaps.
1462
+
1463
+ **Step 3: Verify the gem builds**
1464
+
1465
+ ```bash
1466
+ gem build midwire_common.gemspec
1467
+ ```
1468
+
1469
+ Expected: `midwire_common-2.0.0.gem` built successfully.
1470
+
1471
+ **Step 4: Clean up gem file**
1472
+
1473
+ ```bash
1474
+ rm midwire_common-2.0.0.gem
1475
+ ```
1476
+
1477
+ ---
1478
+
1479
+ ## Task Dependency Graph
1480
+
1481
+ ```
1482
+ Task 1 (infrastructure) ──→ Task 2 (spec_helper) ──→ Task 3 (String) ──→ Task 4 (Array)
1483
+ │ │
1484
+ ├→ Task 5 (Hash) │
1485
+ ├→ Task 6 (Integer) │
1486
+ ├→ Task 7 (Float) │
1487
+ ├→ Task 8 (Enum) │
1488
+ ├→ Task 9 (Time) │
1489
+ └→ Task 10 (File) │
1490
+
1491
+ Tasks 4-10 all complete ──→ Task 11 (all.rb) ──→ Task 12 (docs) ──→ Task 13 (verify)
1492
+ ```
1493
+
1494
+ Tasks 4-10 can be executed in parallel after Task 3 completes (they don't depend on each other). Task 11 depends on all of them completing.