mharris_ext 1.7.0 → 1.8.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 8b1c00fe9fc9db8c94212b12960e5cf57ca42ff0c9201e85931cbdbcc2a66142
4
+ data.tar.gz: 6e3bf943aa7e4dedf2ee76dade33a4d1008e4f7d6ff2a55f22c291cdba0cc800
5
+ SHA512:
6
+ metadata.gz: 3db5d208524aaa9447723af3e2dce5ed3f6c67d92c6c037784cc2473f9fc2a6cc8ad5766594d775feee7a5b3ebfa2c3742708f1d44b61d529054924488db2692
7
+ data.tar.gz: aa2cd71652e5b1dc264f33340e91ec54764faf8b91985154cd11bb1419e6ff0881ff246bec259dfcefee0ece5d739f893e327817544019560cec5563fa5086c1
data/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ source 'https://rubygems.org'
2
+ ruby '>= 2.7'
3
+ gemspec
4
+
5
+ group :test do
6
+ gem 'minitest', '~> 5.25'
7
+ gem 'simplecov', '~> 0.22', require: false
8
+ gem 'activesupport', '>= 7.0', '< 8.2'
9
+ end
data/README CHANGED
@@ -1,7 +1,102 @@
1
- mharris_ext===========
2
- Description goes here.
1
+ = mharris_ext
3
2
 
4
- COPYRIGHT
5
- =========
3
+ Small Ruby helpers used by the playoff-odds applications. Requiring the gem adds
4
+ core extensions and top-level helpers. Ruby 2.7 or newer is required; the local
5
+ development setup uses Ruby 3.3.7.
6
6
 
7
- Copyright (c) 2008 Mike Harris. See LICENSE for details.
7
+ == Development
8
+
9
+ bundle install
10
+ bundle exec rake test
11
+ bundle exec rake build
12
+
13
+ Tests use Minitest and SimpleCov, require no services, and include Active Support
14
+ load-order checks. Coverage is written to coverage/index.html. The built gem is
15
+ written to pkg/. VERSION is the authoritative version; VERSION.yml is retained
16
+ for compatibility with older tooling. Jeweler, RCov and the placeholder Cucumber
17
+ task are no longer part of the build or test workflow.
18
+
19
+ The only runtime dependency is fattr. File.write is provided by Ruby, so Facets
20
+ is no longer required. Bundler 2.4 or newer is recommended for development and
21
+ for commands that use ec's unbundled child environment.
22
+
23
+ == Initialization and lazy attributes
24
+
25
+ require 'mharris_ext'
26
+
27
+ class Options
28
+ include FromHash
29
+ attr_accessor :name, :enabled
30
+ fattr(:retries) { 3 }
31
+ end
32
+
33
+ options = Options.new(name: 'example', enabled: false)
34
+ options.from_hash(name: 'renamed') # updates and returns the same object
35
+ copy = Options.from_hash(name: 'another')
36
+
37
+ Unknown attributes raise rather than being ignored. attr_accessor_nn :value
38
+ provides an accessor that rejects nil when read; false and zero are valid values.
39
+
40
+ == Collections and formatting
41
+
42
+ 3.of { [] } # three distinct arrays
43
+ (1..7).nths(3) # [[1, 2], [3, 4, 5], [6, 7]]
44
+ [1, 2, 3].nths_hash(2) # {0 => [1, 2], 1 => [3]}
45
+ [1, 2, 3].sum_b { |x| x * x } # 14
46
+ [1, 2, 3].avg_b { |x| x * x } # 4.666...
47
+ {a: 1, b: 2}.map_value { |x| x * 2 }
48
+ (-1234567).commify # "-1,234,567"
49
+ '1234.50'.commify # "1,234.50"
50
+ 'x'.rpad(3) # "x "
51
+ 12.lpad(4) # "0012"
52
+
53
+ Partition counts must be positive integers. Partitions preserve input order,
54
+ include every element, and may be empty when more partitions than elements are
55
+ requested. nths_hashx remains an alias of nths_hash. An empty avg_b retains its
56
+ existing NaN result.
57
+
58
+ blank? and present? respect existing implementations, including Active Support.
59
+ When Active Support is absent, nil, false, empty collections and whitespace-only
60
+ strings are blank. Ruby's native Object#tap is preserved.
61
+
62
+ == Files and commands
63
+
64
+ File.create('example.txt', 'first')
65
+ File.append('example.txt', '-last')
66
+ mkdir_recursive('tmp/nested/path')
67
+ mkdir_recursive(dir: 'tmp/another/path')
68
+ mv_making_dir('example.txt', 'tmp/archive')
69
+ rm_r_if('tmp/archive')
70
+ ec('ruby --version', silent: true)
71
+ tm('work') { perform_work }
72
+
73
+ Directory helpers load FileUtils themselves. ec executes a shell command,
74
+ returns stdout, and raises on a nonzero status. When Bundler is loaded, its
75
+ with_unbundled_env API isolates the child command from the current bundle.
76
+ Without silent: true, the command and stdout are printed.
77
+
78
+ Other helpers include Regexp.escaped, Time#short_dt, Object#local_methods,
79
+ all_dirs_recursive, eat_exceptions, bt, and print_memory_usage!. The memory
80
+ reporter returns a Thread and reports resident memory in kilobytes on systems
81
+ with ps; stop and join that thread when finished.
82
+
83
+ == 1.8.0 compatibility fixes
84
+
85
+ * Removed the obsolete Facets File.write require and dependency.
86
+ * Integer#of replaces the defunct Fixnum extension.
87
+ * File helpers use File.exist? and load their standard-library dependency.
88
+ * ec uses current Bundler APIs; timing uses a monotonic clock.
89
+ * Fixed the memory reporter's removed String#to_a call.
90
+ * Preserved Ruby's tap and existing Active Support blank?/present? methods.
91
+ * Fixed false-valued non-nil accessors and the FromHash class factory.
92
+ * Consolidated partition code, supported general Enumerable inputs, and moved
93
+ load-time assertions into tests while keeping the old alias available.
94
+ * Fixed comma formatting for signed numbers and decimal fractions.
95
+
96
+ Validated on Ruby 3.3.7 with Active Support 8.1.3.1. The built package also passed
97
+ isolated smoke checks on Ruby 3.0.6, 3.2.2 and 3.3.7. Both the simulation library's
98
+ 269 examples and the Rails application's 9 integration tests also passed with
99
+ this checkout on the Ruby load path. Consumers must update their dependency
100
+ requirements and lockfiles to use this release.
101
+
102
+ Copyright (c) 2008 Mike Harris. See LICENSE for the MIT license.
data/Rakefile CHANGED
@@ -1,49 +1,9 @@
1
- require 'rake'
2
-
3
- begin
4
- require 'jeweler'
5
- Jeweler::Tasks.new do |s|
6
- s.name = "mharris_ext"
7
- s.summary = %Q{mharris717 utility methods}
8
- s.email = "mharris717@gmail.com"
9
- s.homepage = "http://github.com/GFunk911/mharris_ext"
10
- s.description = "mharris717 utlity methods"
11
- s.authors = ["Mike Harris"]
12
- s.add_dependency 'fattr'
13
- s.add_dependency 'facets'
14
- end
15
- rescue LoadError
16
- puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
17
- end
18
-
19
-
20
-
1
+ require 'bundler/gem_tasks'
21
2
  require 'rake/testtask'
3
+
22
4
  Rake::TestTask.new(:test) do |t|
23
5
  t.libs << 'lib' << 'test'
24
6
  t.pattern = 'test/**/*_test.rb'
25
- t.verbose = false
26
7
  end
27
8
 
28
- begin
29
- require 'rcov/rcovtask'
30
- Rcov::RcovTask.new do |t|
31
- t.libs << 'test'
32
- t.test_files = FileList['test/**/*_test.rb']
33
- t.verbose = true
34
- end
35
- rescue LoadError
36
- puts "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
37
- end
38
-
39
- begin
40
- require 'cucumber/rake/task'
41
- Cucumber::Rake::Task.new(:features)
42
- rescue LoadError
43
- puts "Cucumber is not available. In order to run features, you must: sudo gem install cucumber"
44
- end
45
-
46
- task :default => :test
47
-
48
- Jeweler::GemcutterTasks.new
49
-
9
+ task default: :test
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.8.0
data/VERSION.yml CHANGED
@@ -1,5 +1,5 @@
1
1
  ---
2
2
  :major: 1
3
- :minor: 7
3
+ :minor: 8
4
4
  :patch: 0
5
5
  :build:
@@ -2,7 +2,7 @@ class Object
2
2
  def attr_accessor_nn_one(sym)
3
3
  define_method(sym) do
4
4
  res = instance_variable_get("@#{sym}")
5
- raise "method #{sym} cannot return nil value" unless res
5
+ raise "method #{sym} cannot return nil value" if res.nil?
6
6
  res
7
7
  end
8
8
  attr_writer(sym)
@@ -10,4 +10,4 @@ class Object
10
10
  def attr_accessor_nn(*args)
11
11
  args.flatten.each { |x| attr_accessor_nn_one(x) }
12
12
  end
13
- end
13
+ end
@@ -1,7 +1,7 @@
1
- def tm(msg="Thing")
2
- t = Time.now
1
+ def tm(msg = "Thing")
2
+ t = Process.clock_gettime(Process::CLOCK_MONOTONIC)
3
3
  res = yield
4
- seconds = Time.now - t
4
+ seconds = Process.clock_gettime(Process::CLOCK_MONOTONIC) - t
5
5
  puts "#{msg} took #{seconds} seconds"
6
6
  res
7
7
  end
@@ -9,7 +9,7 @@ end
9
9
  def print_memory_usage!
10
10
  Thread.new do
11
11
  loop do
12
- mem = `ps -l #{Process.pid}`.to_a[1].split[8]
12
+ mem = `ps -o rss= -p #{Process.pid}`.strip
13
13
  puts "Memory: #{mem} #{Time.now}"
14
14
  sleep(10)
15
15
  end
@@ -1,22 +1,25 @@
1
1
  def has_bundler?
2
- Bundler
3
- true
4
- rescue => exp
5
- return false
2
+ !!defined?(Bundler)
6
3
  end
7
4
 
8
- def ec(cmd,ops={})
9
- puts cmd unless ops[:silent]
10
- res = nil
11
- if has_bundler?
12
- Bundler.with_clean_env do
5
+ module MharrisExt
6
+ def self.ec(cmd,ops = {})
7
+ puts cmd unless ops[:silent]
8
+ run = proc do
13
9
  res = `#{cmd}`
10
+ [res, $?]
14
11
  end
15
- else
16
- res = `#{cmd}`
12
+ res, status = if has_bundler?
13
+ Bundler.with_unbundled_env(&run)
14
+ else
15
+ run.call
16
+ end
17
+ raise "bad cmd #{status.to_i} #{cmd} #{res}" unless status.success?
18
+ puts res unless ops[:silent]
19
+ res
17
20
  end
21
+ end
18
22
 
19
- raise "bad cmd #{$?.to_i} #{cmd} #{res}" unless $?.to_i == 0
20
- puts res unless ops[:silent]
21
- res
23
+ def ec(*args)
24
+ MharrisExt.ec(*args)
22
25
  end
@@ -1,7 +1,7 @@
1
- class Fixnum
1
+ class Integer
2
2
  def of
3
3
  res = []
4
- self.times { res << yield }
4
+ times { res << yield }
5
5
  res
6
6
  end
7
7
  end
@@ -12,80 +12,29 @@ module Enumerable
12
12
  each { |x| res += x }
13
13
  res
14
14
  end
15
+
15
16
  def sum_b
16
17
  map { |x| yield(x) }.sum
17
18
  end
19
+
18
20
  def avg_b(&b)
19
21
  sum_b(&b).to_f / size.to_f
20
22
  end
21
- end
22
23
 
23
- module Enumerable
24
- def nths_hashx(num)
25
- res = {}
26
- s = e_f = e_i = 0
27
- n_size = size.to_f / num.to_f
28
- total_segment_size = 0
29
- (0...num).each do |n|
30
- s = e_i
31
- e_f = e_f + n_size
32
- e_i = (e_f+0.5).to_i
33
- seg = self[s...e_i]
34
- # puts %w(s e_f e_i seg).map { |x| "#{x}: #{send(x)}" }.join("\n")
35
- #puts "s: #{s}, e_f: #{e_f}, e_i: #{e_i}, seg: #{seg.inspect}"
36
- res[n] = seg
37
- total_segment_size += res[n].size
38
- end
39
- unless total_segment_size == size
40
- puts inspect
41
- puts res.inspect
42
- raise "nths_hash is bad"
43
- end
44
- res
45
- end
46
24
  def nths(num)
47
- res = []
48
- s = e_f = e_i = 0
49
- n_size = size.to_f / num.to_f
50
- total_segment_size = 0
51
- (0...num).each do |n|
52
- s = e_i
53
- e_f = e_f + n_size
54
- e_i = (e_f+0.5).to_i
55
- seg = self[s...e_i]
56
- # puts %w(s e_f e_i seg).map { |x| "#{x}: #{send(x)}" }.join("\n")
57
- #puts "s: #{s}, e_f: #{e_f}, e_i: #{e_i}, seg: #{seg.inspect}"
58
- res << seg
59
- total_segment_size += res[n].size
25
+ raise ArgumentError, 'number of partitions must be a positive integer' unless num.is_a?(Integer) && num > 0
26
+ items = to_a
27
+ size_per = items.size.fdiv(num)
28
+ Array.new(num) do |i|
29
+ items[(i * size_per).round...((i + 1) * size_per).round]
60
30
  end
61
- unless total_segment_size == size
62
- puts inspect
63
- puts res.inspect
64
- raise "nths_hash is bad"
65
- end
66
- res
67
31
  end
32
+
68
33
  def nths_hash(num)
69
- res = {}
70
- nths(num).each_with_index { |x,i| res[i] = x }
71
- res
34
+ nths(num).each_with_index.to_h { |x,i| [i,x] }
72
35
  end
73
- end
74
-
75
- res = [1,2,3,4,5,6].nths_hash(3)
76
- exp = {0 => [1,2], 1 => [3,4], 2 => [5,6]}
77
- unless res == exp
78
- puts res.inspect
79
- puts exp.inspect
80
- raise "nths_hash doesn't work"
81
- end
82
36
 
83
- res = [1,2,3,4,5,6,7].nths_hash(3)
84
- exp = {0 => [1,2], 1 => [3,4,5], 2 => [6,7]}
85
- unless res == exp
86
- puts res.inspect
87
- puts exp.inspect
88
- raise "nths_hash doesn't work"
37
+ alias_method :nths_hashx, :nths_hash
89
38
  end
90
39
 
91
40
  class Hash
@@ -104,8 +53,13 @@ end
104
53
 
105
54
  class String
106
55
  def commify
107
- return self if length <= 3
108
- self[0...-3].commify + "," + self[-3..-1]
56
+ if (parts = /\A([+-]?)(\d+)(\.\d+)?\z/.match(self))
57
+ digits = parts[2].reverse.scan(/.{1,3}/).join(',').reverse
58
+ "#{parts[1]}#{digits}#{parts[3]}"
59
+ else
60
+ # Preserve the legacy grouping behavior for nonnumeric strings.
61
+ return self if length <= 3
62
+ self[0...-3].commify + ',' + self[-3..-1]
63
+ end
109
64
  end
110
65
  end
111
-
@@ -1,8 +1,7 @@
1
+ require 'fileutils'
2
+
1
3
  def all_dirs_recursive(dir)
2
4
  raise "null dir" unless dir
3
- init_slash = (dir[0..0] == '/')
4
- #raise "can't handle initial slash" if dir[0..0] == '/'
5
- #puts "all_dirs_recursive #{dir}"
6
5
  dir.split("/")[0..-1].inject([]) do |paths,dir|
7
6
  last_path = (paths.empty? ? "" : "#{paths[-1]}/")
8
7
  paths + ["#{last_path}#{dir}"]
@@ -10,43 +9,26 @@ def all_dirs_recursive(dir)
10
9
  end
11
10
 
12
11
  def mkdir_if(dir)
13
- if FileTest.exists?(dir)
14
- #puts "not making #{dir}"
15
- else
16
- #puts "making #{dir}"
17
- FileUtils.mkdir(dir)
18
- end
12
+ FileUtils.mkdir(dir) unless File.exist?(dir)
19
13
  end
20
14
 
21
15
  def mkdir_recursive(ops)
22
16
  dir = ops.is_a?(Hash) ? ops[:dir] : ops
23
- #puts "mkdir_recursive #{dir}"
24
17
  all_dirs_recursive(dir).each do |dir|
25
18
  mkdir_if(dir)
26
19
  end
27
20
  end
28
21
 
29
22
  def mv_making_dir(f,new_dir)
30
- #puts "mv_making_dir #{f} #{new_dir}"
31
23
  mkdir_recursive(new_dir)
32
24
  FileUtils.mv(f,new_dir)
33
25
  end
34
26
 
35
- unless all_dirs_recursive("a/b/c") == ["a","a/b","a/b/c"]
36
- res = all_dirs_recursive("a/b/c")
37
- raise "all_dirs_recursive doesn't work #{res.inspect}"
38
- end
39
-
40
- unless all_dirs_recursive("/a/b/c") == ["/a","/a/b","/a/b/c"]
41
- res = all_dirs_recursive("/a/b/c")
42
- raise "all_dirs_recursive doesn't work #{res.inspect}"
43
- end
44
-
45
27
  def rm_r_if(dir)
46
- FileUtils.rm_r(dir) if FileTest.exists?(dir)
28
+ FileUtils.rm_r(dir) if File.exist?(dir)
47
29
  end
48
30
 
49
31
  def eat_exceptions
50
32
  yield
51
33
  rescue
52
- end
34
+ end
@@ -1,19 +1,22 @@
1
1
  module FromHash
2
+ def self.included(base)
3
+ base.extend(ClassMethods)
4
+ end
5
+
6
+ module ClassMethods
7
+ def from_hash(ops = {})
8
+ new.from_hash(ops)
9
+ end
10
+ end
11
+
2
12
  def from_hash(ops)
3
13
  ops.each do |k,v|
4
14
  send("#{k}=",v)
5
15
  end
6
16
  self
7
17
  end
8
- def initialize(ops={})
18
+
19
+ def initialize(ops = {})
9
20
  from_hash(ops)
10
21
  end
11
22
  end
12
-
13
- class Class
14
- def self.from_hash(ops={})
15
- res = new
16
- res.from_hash(ops)
17
- res
18
- end
19
- end
@@ -1,2 +1 @@
1
- require 'facets/file/write'
2
- require 'fattr'
1
+ require 'fattr'
@@ -1,6 +1 @@
1
- class Object
2
- def tap
3
- yield(self)
4
- self
5
- end
6
- end
1
+ # Object#tap is provided by Ruby. Keep this require path for compatibility.
@@ -1,8 +1,17 @@
1
1
  class Object
2
- def blank?
3
- to_s.strip == ''
2
+ unless method_defined?(:blank?)
3
+ def blank?
4
+ if is_a?(String)
5
+ /\A[[:space:]]*\z/.match?(self)
6
+ else
7
+ respond_to?(:empty?) ? !!empty? : !self
8
+ end
9
+ end
4
10
  end
5
- def present?
6
- !blank?
11
+
12
+ unless method_defined?(:present?)
13
+ def present?
14
+ !blank?
15
+ end
7
16
  end
8
- end
17
+ end
data/lib/mharris_ext.rb CHANGED
@@ -1,2 +1,2 @@
1
- glob = File.dirname(__FILE__) + "/mharris_ext/*.rb"
2
- Dir[glob].each { |x| require x }
1
+ require_relative 'mharris_ext/gems'
2
+ Dir[File.join(__dir__, 'mharris_ext', '*.rb')].sort.each { |x| require x }
data/mharris_ext.gemspec CHANGED
@@ -1,67 +1,17 @@
1
- # Generated by jeweler
2
- # DO NOT EDIT THIS FILE DIRECTLY
3
- # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
- # -*- encoding: utf-8 -*-
5
-
6
1
  Gem::Specification.new do |s|
7
- s.name = "mharris_ext"
8
- s.version = "1.7.0"
9
-
10
- s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
- s.authors = ["Mike Harris"]
12
- s.date = "2013-04-12"
13
- s.description = "mharris717 utlity methods"
14
- s.email = "mharris717@gmail.com"
15
- s.extra_rdoc_files = [
16
- "LICENSE",
17
- "README"
18
- ]
19
- s.files = [
20
- "LICENSE",
21
- "README",
22
- "Rakefile",
23
- "VERSION.yml",
24
- "features/mharris_ext.feature",
25
- "features/steps/mharris_ext_steps.rb",
26
- "features/support/env.rb",
27
- "lib/mharris_ext.rb",
28
- "lib/mharris_ext/accessor.rb",
29
- "lib/mharris_ext/benchmark.rb",
30
- "lib/mharris_ext/cmd.rb",
31
- "lib/mharris_ext/enumerable.rb",
32
- "lib/mharris_ext/file.rb",
33
- "lib/mharris_ext/fileutils.rb",
34
- "lib/mharris_ext/from_hash.rb",
35
- "lib/mharris_ext/gems.rb",
36
- "lib/mharris_ext/methods.rb",
37
- "lib/mharris_ext/object.rb",
38
- "lib/mharris_ext/present.rb",
39
- "lib/mharris_ext/regexp.rb",
40
- "lib/mharris_ext/string.rb",
41
- "lib/mharris_ext/time.rb",
42
- "lib/mharris_ext/trace.rb",
43
- "mharris_ext.gemspec",
44
- "test/mharris_ext_test.rb",
45
- "test/test_helper.rb"
46
- ]
47
- s.homepage = "http://github.com/GFunk911/mharris_ext"
48
- s.require_paths = ["lib"]
49
- s.rubygems_version = "1.8.23"
50
- s.summary = "mharris717 utility methods"
51
-
52
- if s.respond_to? :specification_version then
53
- s.specification_version = 3
54
-
55
- if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
56
- s.add_runtime_dependency(%q<fattr>, [">= 0"])
57
- s.add_runtime_dependency(%q<facets>, [">= 0"])
58
- else
59
- s.add_dependency(%q<fattr>, [">= 0"])
60
- s.add_dependency(%q<facets>, [">= 0"])
61
- end
62
- else
63
- s.add_dependency(%q<fattr>, [">= 0"])
64
- s.add_dependency(%q<facets>, [">= 0"])
65
- end
2
+ s.name = 'mharris_ext'
3
+ s.version = File.read(File.expand_path('VERSION', __dir__)).strip
4
+ s.summary = 'Mike Harris Ruby utility methods'
5
+ s.description = 'Small Ruby helpers for object initialization, collections, files, and shell commands.'
6
+ s.authors = ['Mike Harris']
7
+ s.email = 'mharris717@gmail.com'
8
+ s.homepage = 'https://github.com/mharris717/mharris_ext'
9
+ s.license = 'MIT'
10
+ s.required_ruby_version = '>= 2.7'
11
+ s.require_paths = ['lib']
12
+ s.files = Dir['lib/**/*.rb', 'test/**/*.rb'] + %w[LICENSE README Rakefile VERSION VERSION.yml Gemfile mharris_ext.gemspec]
13
+ s.metadata = { 'source_code_uri' => s.homepage }
14
+ s.add_dependency 'fattr', '~> 2.4'
15
+ s.add_development_dependency 'bundler', '>= 2.4'
16
+ s.add_development_dependency 'rake', '~> 13.3'
66
17
  end
67
-
@@ -0,0 +1,247 @@
1
+ require_relative 'test_helper'
2
+
3
+ class CollectionsTest < Minitest::Test
4
+ def test_integer_of_returns_fresh_results
5
+ rows = 3.of { [] }
6
+ assert_equal [[], [], []], rows
7
+ assert_equal 3, rows.map(&:object_id).uniq.size
8
+ assert_equal [], 0.of { flunk }
9
+ end
10
+
11
+ def test_partition_distribution_and_legacy_alias
12
+ assert_equal [[1, 2], [3, 4], [5, 6]], (1..6).to_a.nths(3)
13
+ assert_equal [[1, 2], [3, 4, 5], [6, 7]], (1..7).to_a.nths(3)
14
+ assert_equal [[], [1], [], [2], []], [1, 2].nths(5)
15
+ assert_equal [[], [], []], [].nths(3)
16
+ expected = {0 => [1, 2], 1 => [3, 4, 5], 2 => [6, 7]}
17
+ assert_equal expected, (1..7).to_a.nths_hash(3)
18
+ assert_equal expected, (1..7).to_a.nths_hashx(3)
19
+ end
20
+
21
+ def test_partitions_any_enumerable_without_losing_elements
22
+ assert_equal [[1, 2], [3, 4, 5], [6, 7]], (1..7).nths(3)
23
+ (1..17).each do |n|
24
+ parts = (1..31).each.nths(n)
25
+ assert_equal n, parts.size
26
+ assert_equal (1..31).to_a, parts.flatten
27
+ assert_operator parts.map(&:size).max - parts.map(&:size).min, :<=, 1
28
+ end
29
+ end
30
+
31
+ def test_invalid_partition_counts_are_rejected
32
+ [0, -1, 1.5, '2', nil].each do |n|
33
+ assert_raises(ArgumentError) { [1, 2].nths(n) }
34
+ end
35
+ end
36
+
37
+ def test_aggregation_and_hash_mapping
38
+ assert_equal 6, [1, 2, 3].sumx
39
+ assert_equal 14, [1, 2, 3].sum_b { |x| x * x }
40
+ assert_in_delta 14.0 / 3, [1, 2, 3].avg_b { |x| x * x }
41
+ assert_equal({a: 2, b: 4}, {a: 1, b: 2}.map_value { |x| x * 2 })
42
+ end
43
+ end
44
+
45
+ class ObjectHelpersTest < Minitest::Test
46
+ def test_from_hash_initialization_updates_and_factory
47
+ klass = Class.new do
48
+ include FromHash
49
+ attr_accessor :name, :active
50
+ end
51
+ x = klass.new(name: 'one', active: false)
52
+ assert_equal 'one', x.name
53
+ assert_equal false, x.active
54
+ assert_same x, x.from_hash('name' => 'two')
55
+ assert_equal 'two', x.name
56
+ assert_equal 'three', klass.from_hash(name: 'three').name
57
+ assert_raises(NoMethodError) { klass.new(unknown: 1) }
58
+ end
59
+
60
+ def test_non_nil_accessor_accepts_false
61
+ klass = Class.new { attr_accessor_nn :value }
62
+ x = klass.new
63
+ assert_raises(RuntimeError) { x.value }
64
+ x.value = false
65
+ assert_equal false, x.value
66
+ x.value = 0
67
+ assert_equal 0, x.value
68
+ x.value = nil
69
+ assert_raises(RuntimeError) { x.value }
70
+ end
71
+
72
+ def test_blank_and_present_without_active_support
73
+ [nil, false, '', ' ', "\u2003", [], {}].each do |x|
74
+ assert x.blank?, "#{x.inspect} should be blank"
75
+ refute x.present?
76
+ end
77
+ [true, 0, '0', [nil], {a: nil}, Object.new].each do |x|
78
+ refute x.blank?, "#{x.inspect} should be present"
79
+ assert x.present?
80
+ end
81
+ x = Object.new
82
+ def x.empty?; true; end
83
+ assert x.blank?
84
+ end
85
+
86
+ def test_preserves_ruby_tap
87
+ out, = run_ruby(<<~RUBY)
88
+ before = Object.instance_method(:tap)
89
+ require 'mharris_ext'
90
+ raise 'tap was replaced' unless Object.instance_method(:tap) == before
91
+ x = Object.new
92
+ raise unless x.tap { |v| raise unless v.equal?(x) }.equal?(x)
93
+ puts 'native tap'
94
+ RUBY
95
+ assert_equal "native tap\n", out
96
+ end
97
+
98
+ def test_active_support_load_order
99
+ [true, false].each do |rails_first|
100
+ code = <<~RUBY
101
+ require 'active_support/core_ext/object/blank' if #{rails_first}
102
+ before = Object.instance_method(:blank?) if #{rails_first}
103
+ require 'mharris_ext'
104
+ raise 'Active Support blank? replaced' if #{rails_first} && Object.instance_method(:blank?) != before
105
+ require 'active_support/core_ext/object/blank'
106
+ [nil, false, '', ' ', [], {}].each { |x| raise x.inspect unless x.blank? && !x.present? }
107
+ raise unless 0.present?
108
+ puts 'compatible'
109
+ RUBY
110
+ out, = run_ruby(code)
111
+ assert_equal "compatible\n", out
112
+ end
113
+ end
114
+
115
+ def test_local_methods
116
+ x = Class.new { def special_helper; end }.new
117
+ assert_includes x.local_methods, :special_helper
118
+ end
119
+ end
120
+
121
+ class FormattingTest < Minitest::Test
122
+ def test_padding_preserves_existing_call_shapes
123
+ assert_equal 'x ', 'x'.rpad(3)
124
+ assert_equal ' x', 'x'.lpad(3)
125
+ assert_equal 'long', 'long'.rpad(2)
126
+ assert_equal 'x....', 'x'.rpad(3, '..')
127
+ assert_equal '0012', 12.lpad(4)
128
+ end
129
+
130
+ def test_commify_signed_and_decimal_numbers
131
+ assert_equal '1,234,567', 1234567.commify
132
+ assert_equal '-123', (-123).commify
133
+ assert_equal '-1,234,567', (-1234567).commify
134
+ assert_equal '1,234.56', '1234.56'.commify
135
+ assert_equal '+1,234.50', '+1234.50'.commify
136
+ assert_equal '12', 12.commify
137
+ end
138
+
139
+ def test_regexp_and_time
140
+ pattern = Regexp.escaped('a.b[0]')
141
+ assert_match pattern, 'a.b[0]'
142
+ refute_match pattern, 'axb0'
143
+ assert_equal '09/04 13:02:03', Time.new(2026, 9, 4, 13, 2, 3).short_dt
144
+ end
145
+ end
146
+
147
+ class FileHelpersTest < Minitest::Test
148
+ def test_file_creation_append_and_native_write
149
+ Dir.mktmpdir do |dir|
150
+ file = File.join(dir, 'sample')
151
+ File.create(file, 'first')
152
+ File.append(file, '-last')
153
+ assert_equal 'first-last', File.read(file)
154
+ assert_equal 3, File.write(file, 'new')
155
+ assert_equal 'new', File.read(file)
156
+ end
157
+ end
158
+
159
+ def test_path_expansion
160
+ assert_equal ['a', 'a/b', 'a/b/c'], all_dirs_recursive('a/b/c')
161
+ assert_equal ['/a', '/a/b', '/a/b/c'], all_dirs_recursive('/a/b/c')
162
+ assert_raises(RuntimeError) { all_dirs_recursive(nil) }
163
+ end
164
+
165
+ def test_directory_helpers_use_supported_ruby_file_apis
166
+ Dir.mktmpdir do |dir|
167
+ target = File.join(dir, 'a', 'b')
168
+ mkdir_recursive(dir: target)
169
+ mkdir_recursive(target)
170
+ assert File.directory?(target)
171
+ source = File.join(dir, 'source.txt')
172
+ File.write(source, 'hello')
173
+ mv_making_dir(source, target)
174
+ assert_equal 'hello', File.read(File.join(target, 'source.txt'))
175
+ rm_r_if(File.join(dir, 'a'))
176
+ refute File.exist?(target)
177
+ rm_r_if(File.join(dir, 'missing'))
178
+ end
179
+ end
180
+
181
+ def test_optional_exception_swallowing
182
+ assert_nil eat_exceptions { raise 'expected' }
183
+ assert_equal 42, eat_exceptions { 42 }
184
+ end
185
+ end
186
+
187
+ class CommandHelpersTest < Minitest::Test
188
+ def ruby_command(code)
189
+ Shellwords.join([RbConfig.ruby, '-e', code])
190
+ end
191
+
192
+ def test_success_and_silent_output
193
+ out, err = capture_io do
194
+ assert_equal 'ok', ec(ruby_command("print 'ok'"), silent: true)
195
+ end
196
+ assert_empty out
197
+ assert_empty err
198
+ end
199
+
200
+ def test_nonzero_exit_reports_failure
201
+ error = assert_raises(RuntimeError) { ec(ruby_command("print 'failure'; exit 7"), silent: true) }
202
+ assert_match(/bad cmd/, error.message)
203
+ assert_match(/failure/, error.message)
204
+ end
205
+
206
+ def test_unbundles_child_processes_with_current_bundler
207
+ assert has_bundler?
208
+ assert_equal 'unset', ec(ruby_command("print ENV.fetch('BUNDLE_GEMFILE', 'unset')"), silent: true)
209
+ end
210
+
211
+ def test_also_runs_without_bundler
212
+ out, err, status = Bundler.with_unbundled_env do
213
+ Open3.capture3(RbConfig.ruby, '-Ilib', '-e', <<~RUBY)
214
+ require 'mharris_ext'
215
+ raise if has_bundler?
216
+ print ec(#{ruby_command("print 'ok'").inspect}, silent: true)
217
+ RUBY
218
+ end
219
+ assert status.success?, err
220
+ assert_equal 'ok', out
221
+ end
222
+ end
223
+
224
+ class DiagnosticHelpersTest < Minitest::Test
225
+ def test_timing_returns_the_block_result
226
+ out, = capture_io { assert_equal 42, tm('job') { 42 } }
227
+ assert_match(/job took .* seconds/, out)
228
+ end
229
+
230
+ def test_backtrace_printing
231
+ out, = capture_io { bt }
232
+ assert_includes out, 'test_backtrace_printing'
233
+ end
234
+
235
+ def test_memory_report_thread_can_be_stopped
236
+ x = Object.new
237
+ lines = Queue.new
238
+ x.define_singleton_method(:`) { |_| " 1234\n" }
239
+ x.define_singleton_method(:puts) { |s| lines << s }
240
+ thread = x.send(:print_memory_usage!)
241
+ line = Timeout.timeout(2) { lines.pop }
242
+ assert_match(/Memory: 1234/, line)
243
+ ensure
244
+ thread&.kill
245
+ thread&.join
246
+ end
247
+ end
@@ -1,7 +1,20 @@
1
- require File.dirname(__FILE__) + '/test_helper'
1
+ require_relative 'test_helper'
2
2
 
3
- class MharrisExtTest < Test::Unit::TestCase
4
- should "probably rename this file and start testing for real" do
5
- flunk "hey buddy, you should probably rename this file and start testing for real"
3
+ class MharrisExtTest < Minitest::Test
4
+ def test_loads_without_facets_and_preserves_builtin_file_write
5
+ out, err = run_ruby(<<~RUBY)
6
+ original = File.method(:write)
7
+ require 'mharris_ext'
8
+ raise 'Facets loaded' if $LOADED_FEATURES.any? { |x| x.include?('/facets/') }
9
+ raise 'File.write replaced' unless File.method(:write) == original
10
+ puts 'loaded'
11
+ RUBY
12
+ assert_equal "loaded\n", out
13
+ assert_empty err
6
14
  end
7
- end
15
+
16
+ def test_fattr_is_still_available
17
+ klass = Class.new { fattr(:answer) { 42 } }
18
+ assert_equal 42, klass.new.answer
19
+ end
20
+ end
data/test/test_helper.rb CHANGED
@@ -1,10 +1,24 @@
1
- require 'rubygems'
2
- require 'test/unit'
3
- require 'shoulda'
4
- require 'mocha'
1
+ require 'simplecov'
2
+ SimpleCov.start do
3
+ add_filter '/test/'
4
+ track_files 'lib/**/*.rb'
5
+ end
6
+ require 'minitest/autorun'
7
+ require 'minitest/mock'
8
+ require 'open3'
9
+ require 'rbconfig'
10
+ require 'shellwords'
11
+ require 'tmpdir'
12
+ require 'timeout'
13
+ require 'stringio'
5
14
 
6
- $LOAD_PATH.unshift(File.dirname(__FILE__))
15
+ $LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
7
16
  require 'mharris_ext'
8
17
 
9
- class Test::Unit::TestCase
18
+ class Minitest::Test
19
+ def run_ruby(code)
20
+ out, err, status = Open3.capture3(RbConfig.ruby, '-Ilib', '-e', code)
21
+ assert status.success?, "#{out}\n#{err}"
22
+ [out, err]
23
+ end
10
24
  end
metadata CHANGED
@@ -1,63 +1,70 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mharris_ext
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.7.0
5
- prerelease:
4
+ version: 1.8.0
6
5
  platform: ruby
7
6
  authors:
8
7
  - Mike Harris
9
- autorequire:
8
+ autorequire:
10
9
  bindir: bin
11
10
  cert_chain: []
12
- date: 2013-04-12 00:00:00.000000000 Z
11
+ date: 2026-09-05 00:00:00.000000000 Z
13
12
  dependencies:
14
13
  - !ruby/object:Gem::Dependency
15
14
  name: fattr
16
15
  requirement: !ruby/object:Gem::Requirement
17
- none: false
18
16
  requirements:
19
- - - ! '>='
17
+ - - "~>"
20
18
  - !ruby/object:Gem::Version
21
- version: '0'
19
+ version: '2.4'
22
20
  type: :runtime
23
21
  prerelease: false
24
22
  version_requirements: !ruby/object:Gem::Requirement
25
- none: false
26
23
  requirements:
27
- - - ! '>='
24
+ - - "~>"
28
25
  - !ruby/object:Gem::Version
29
- version: '0'
26
+ version: '2.4'
30
27
  - !ruby/object:Gem::Dependency
31
- name: facets
28
+ name: bundler
32
29
  requirement: !ruby/object:Gem::Requirement
33
- none: false
34
30
  requirements:
35
- - - ! '>='
31
+ - - ">="
36
32
  - !ruby/object:Gem::Version
37
- version: '0'
38
- type: :runtime
33
+ version: '2.4'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '2.4'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '13.3'
48
+ type: :development
39
49
  prerelease: false
40
50
  version_requirements: !ruby/object:Gem::Requirement
41
- none: false
42
51
  requirements:
43
- - - ! '>='
52
+ - - "~>"
44
53
  - !ruby/object:Gem::Version
45
- version: '0'
46
- description: mharris717 utlity methods
54
+ version: '13.3'
55
+ description: Small Ruby helpers for object initialization, collections, files, and
56
+ shell commands.
47
57
  email: mharris717@gmail.com
48
58
  executables: []
49
59
  extensions: []
50
- extra_rdoc_files:
51
- - LICENSE
52
- - README
60
+ extra_rdoc_files: []
53
61
  files:
62
+ - Gemfile
54
63
  - LICENSE
55
64
  - README
56
65
  - Rakefile
66
+ - VERSION
57
67
  - VERSION.yml
58
- - features/mharris_ext.feature
59
- - features/steps/mharris_ext_steps.rb
60
- - features/support/env.rb
61
68
  - lib/mharris_ext.rb
62
69
  - lib/mharris_ext/accessor.rb
63
70
  - lib/mharris_ext/benchmark.rb
@@ -75,30 +82,31 @@ files:
75
82
  - lib/mharris_ext/time.rb
76
83
  - lib/mharris_ext/trace.rb
77
84
  - mharris_ext.gemspec
85
+ - test/helpers_test.rb
78
86
  - test/mharris_ext_test.rb
79
87
  - test/test_helper.rb
80
- homepage: http://github.com/GFunk911/mharris_ext
81
- licenses: []
82
- post_install_message:
88
+ homepage: https://github.com/mharris717/mharris_ext
89
+ licenses:
90
+ - MIT
91
+ metadata:
92
+ source_code_uri: https://github.com/mharris717/mharris_ext
93
+ post_install_message:
83
94
  rdoc_options: []
84
95
  require_paths:
85
96
  - lib
86
97
  required_ruby_version: !ruby/object:Gem::Requirement
87
- none: false
88
98
  requirements:
89
- - - ! '>='
99
+ - - ">="
90
100
  - !ruby/object:Gem::Version
91
- version: '0'
101
+ version: '2.7'
92
102
  required_rubygems_version: !ruby/object:Gem::Requirement
93
- none: false
94
103
  requirements:
95
- - - ! '>='
104
+ - - ">="
96
105
  - !ruby/object:Gem::Version
97
106
  version: '0'
98
107
  requirements: []
99
- rubyforge_project:
100
- rubygems_version: 1.8.23
101
- signing_key:
102
- specification_version: 3
103
- summary: mharris717 utility methods
108
+ rubygems_version: 3.5.22
109
+ signing_key:
110
+ specification_version: 4
111
+ summary: Mike Harris Ruby utility methods
104
112
  test_files: []
@@ -1,9 +0,0 @@
1
- Feature: something something
2
- In order to something something
3
- A user something something
4
- something something something
5
-
6
- Scenario: something something
7
- Given inspiration
8
- When I create a sweet new gem
9
- Then everyone should see how awesome I am
File without changes
@@ -1,13 +0,0 @@
1
- $LOAD_PATH.unshift(File.dirname(__FILE__) + '/../../lib')
2
- require 'mharris_ext'
3
-
4
- require 'test/unit/assertions'
5
-
6
- require 'test/unit/assertions'
7
-
8
- World do |world|
9
-
10
- world.extend(Test::Unit::Assertions)
11
-
12
- world
13
- end