facets 4.0.0 → 4.0.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/CONTRIBUTING.md +8 -2
- data/HISTORY.md +65 -0
- data/README.md +7 -1
- data/lib/core/facets/enumerable/graph.rb +1 -1
- data/lib/core/facets/enumerable/map_by.rb +6 -2
- data/lib/core/facets/kernel/deep_clone.rb +36 -8
- data/lib/core/facets/module/alias_class_method.rb +8 -3
- data/lib/core/facets/module/module_def.rb +0 -11
- data/lib/core/facets/module/redefine_method.rb +12 -3
- data/lib/core/facets/time/round_to.rb +1 -1
- data/lib/core/facets/version.rb +1 -1
- data/lib/standard/facets/cloneable.rb +4 -3
- data/lib/standard/facets/find/select.rb +3 -1
- data/lib/standard/facets/interval.rb +1 -1
- data/lib/standard/facets/math/percentile.rb +9 -4
- data/lib/standard/facets/shellwords.rb +6 -10
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: db8b790b2c8e0551d30a4020f9e9a206306ef7b57dc3bb73cf75ab8bfe192b52
|
|
4
|
+
data.tar.gz: 38d282a641961639027ed77a9042de9f837d55f6d4c37935e502ed71c1b22431
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7e85bd3715fbf1a7c333aefbf9b9db67afe2bd7dfbfe1119a6c2af640e6f5b189518b5c59b47d36effca090d523eaf31bed497028d000691f3c5b26f15012265
|
|
7
|
+
data.tar.gz: d32670ff9724cdd492b10092970ef9cfb4716530c2aa3f6b4906846ca0e8dae45dc04336c30d78f66da41c99e00ce9cc9a23df833dd07cf66c515891e8527945
|
data/CONTRIBUTING.md
CHANGED
|
@@ -104,8 +104,8 @@ explanation if needed, including *when* and *why* the method could be useful.
|
|
|
104
104
|
|
|
105
105
|
The test suite uses four tools:
|
|
106
106
|
|
|
107
|
-
* **RubyTest** provides the test runner interface
|
|
108
|
-
|
|
107
|
+
* **RubyTest** provides the test runner interface and the `rubytest` command
|
|
108
|
+
used by the Rake tasks.
|
|
109
109
|
* **Lemon** defines the `test_case`, `method`, and `test` structure of the unit
|
|
110
110
|
tests.
|
|
111
111
|
* **AE** provides assertions such as `.assert` and `expect`, used in both the
|
|
@@ -130,6 +130,12 @@ TESTS=test/core/array/test_average.rb bundle exec rake test
|
|
|
130
130
|
The Rakefile also provides `test:core`, `test:standard`, `test:rails` and the
|
|
131
131
|
corresponding `qed:*` tasks for each library area.
|
|
132
132
|
|
|
133
|
+
Tests and demos of deprecated methods call them on purpose, so both suites hide
|
|
134
|
+
those deprecation warnings and print a count at the end instead
|
|
135
|
+
(`etc/quiet_deprecations.rb`). A deprecation warning raised from inside `lib/`
|
|
136
|
+
still shows, because it means Facets calls a deprecated method itself. To see
|
|
137
|
+
every warning, set `FACETS_DEPRECATIONS=show`.
|
|
138
|
+
|
|
133
139
|
* A core method in `lib/core/facets/{class}/{method}.rb` normally has a Lemon
|
|
134
140
|
test in `test/core/{class}/test_{method}.rb` and a QED demo in
|
|
135
141
|
`demo/core/{class}/{method}.md`. Standard and rails libraries use
|
data/HISTORY.md
CHANGED
|
@@ -1,5 +1,70 @@
|
|
|
1
1
|
# Facets Release History
|
|
2
2
|
|
|
3
|
+
## 4.0.1 / 2026-11-02
|
|
4
|
+
|
|
5
|
+
A patch release with a security fix: `require 'facets/shellwords'` replaced
|
|
6
|
+
Ruby's `Shellwords.escape` with a version that left shell metacharacters
|
|
7
|
+
unescaped. It also lets Rails apps that load Facets boot again, and fixes
|
|
8
|
+
several methods that broke on Ruby 3.
|
|
9
|
+
|
|
10
|
+
Changes:
|
|
11
|
+
|
|
12
|
+
* Bug Fixes
|
|
13
|
+
|
|
14
|
+
* Security: `require 'facets/shellwords'` replaced Ruby's
|
|
15
|
+
`Shellwords.escape`, and with it `String#shellescape`, with a fallback
|
|
16
|
+
that escaped almost nothing. `Shellwords.escape("x; rm -rf ~")` came back
|
|
17
|
+
unchanged, so code that relied on it to quote shell arguments was open to
|
|
18
|
+
command injection. The fallback was meant to apply only where Ruby had no
|
|
19
|
+
`escape`, but its check never matched Ruby's definition, so it always
|
|
20
|
+
applied. It is gone, and Ruby's own `escape` is always used. The bug dates
|
|
21
|
+
back to 2010. `require 'facets'` does not load this file (#67).
|
|
22
|
+
* `require 'facets'` no longer makes `attr`, `attr_reader`, `attr_writer`,
|
|
23
|
+
`attr_accessor`, `remove_method` and `undef_method` protected; Ruby has
|
|
24
|
+
made them public since 2.5. `Module#redefine_method` is now public, like
|
|
25
|
+
ActiveSupport's, so Rails apps that load Facets no longer fail with
|
|
26
|
+
"private method 'redefine_method' called" (#292). A redefined method now
|
|
27
|
+
keeps its visibility; before, a redefined private method became public.
|
|
28
|
+
* `Enumerable#graph` (also `Hash#graph` and `#mash`) and `Interval#step`
|
|
29
|
+
without a block, and every call to `Find.select`, no longer raise
|
|
30
|
+
`ArgumentError` on Ruby 3 (#246). `facets/find/select` now also requires
|
|
31
|
+
`find` itself.
|
|
32
|
+
* `Math.percentile` returns the minimum or maximum at the extremes, as the
|
|
33
|
+
NIST method it cites specifies. `percentile([1,2,3], 90)` raised
|
|
34
|
+
`NoMethodError`, `percentile([1,2,3], 100)` returned `nil` and
|
|
35
|
+
`percentile([1,2,3], 0)` returned 3 (#289).
|
|
36
|
+
* `Kernel#deep_clone` now copies array elements and hash values at any
|
|
37
|
+
depth; before, it only copied instance variables (#11). Equal but distinct
|
|
38
|
+
objects, such as two equal strings, no longer share one copy. Hash keys,
|
|
39
|
+
and Struct and Set members, are still shared with the original.
|
|
40
|
+
* `Enumerable#map_by` maps `false` and `nil` values instead of storing the
|
|
41
|
+
whole element (#75).
|
|
42
|
+
* `Module#alias_class_method` works. It raised `NameError` on every call.
|
|
43
|
+
* `Time#round_to` no longer prints a deprecation warning. It called the
|
|
44
|
+
deprecated `Time#trunc`.
|
|
45
|
+
* The `facets/cloneable` deprecation warning recommends `Kernel#deep_clone`
|
|
46
|
+
or `initialize_copy` instead of `deep_dup`, which Facets doesn't define.
|
|
47
|
+
The 3.2.0 notes gave the same wrong advice.
|
|
48
|
+
|
|
49
|
+
* Documentation
|
|
50
|
+
|
|
51
|
+
* The README names the current release (it still said 3.2.2) and shows how
|
|
52
|
+
to load chosen extensions from a Gemfile with `require: [...]` (#280).
|
|
53
|
+
* The `Shellwords.alt_escape` docs no longer suggest it for `system()`. It
|
|
54
|
+
leaves `$ ; * ? ~` and newlines unescaped, so it is not safe for untrusted
|
|
55
|
+
input.
|
|
56
|
+
|
|
57
|
+
* Internal
|
|
58
|
+
|
|
59
|
+
* The tests use rubytest 0.9's own `rubytest` command instead of the
|
|
60
|
+
`rubytest-cli` gem.
|
|
61
|
+
* The test and demo runs hide the deprecation warnings they trigger on
|
|
62
|
+
purpose and print a count instead. A deprecation warning raised from
|
|
63
|
+
inside `lib/` still shows. Set `FACETS_DEPRECATIONS=show` to see every
|
|
64
|
+
warning.
|
|
65
|
+
* First tests for `Interval#step`, `Find.select` and
|
|
66
|
+
`Module#alias_class_method`.
|
|
67
|
+
|
|
3
68
|
## 4.0.0 / 2026-09-27
|
|
4
69
|
|
|
5
70
|
A major release, because it changes behavior: `Kernel#try!` is now the
|
data/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
**More of Ruby, one method at a time.** Facets is a collection of extensions to Ruby's core classes and standard library, plus a few small, reusable classes and modules. Most methods live in their own files, so you can load one extension, a class's extensions, or the core collection.
|
|
7
7
|
|
|
8
|
-
Facets took its name in 2004, growing out of a small methods library started in 2002, and is still maintained. The current release is **
|
|
8
|
+
Facets took its name in 2004, growing out of a small methods library started in 2002, and is still maintained. The current release is **4.0.1**, which requires **Ruby 3.1 or newer**. See the [release history](HISTORY.md) for changes and migration notes from earlier versions. The `main` branch also contains changes awaiting the next release.
|
|
9
9
|
|
|
10
10
|
## Install
|
|
11
11
|
|
|
@@ -21,6 +21,12 @@ gem 'facets', require: false
|
|
|
21
21
|
|
|
22
22
|
`require: false` lets you choose which extensions to load. Omit it if you want Bundler to load the core collection automatically.
|
|
23
23
|
|
|
24
|
+
To have Bundler load only certain extensions, for example in a Rails app, list their files instead:
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
gem 'facets', require: ['facets/array/to_ranges', 'facets/string/snakecase']
|
|
28
|
+
```
|
|
29
|
+
|
|
24
30
|
## Choose how much to load
|
|
25
31
|
|
|
26
32
|
### One method
|
|
@@ -21,10 +21,14 @@ module Enumerable
|
|
|
21
21
|
def map_by #:yield:
|
|
22
22
|
res = {}
|
|
23
23
|
each do |a|
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
r = yield(*a)
|
|
25
|
+
# Test whether a second value was returned, not whether it is truthy,
|
|
26
|
+
# so false and nil are mapped too.
|
|
27
|
+
if Array === r && r.size > 1
|
|
28
|
+
k, v = r
|
|
26
29
|
(res[k] ||= []) << v
|
|
27
30
|
else
|
|
31
|
+
k, = r
|
|
28
32
|
(res[k] ||= []) << a
|
|
29
33
|
end
|
|
30
34
|
end
|
|
@@ -1,24 +1,52 @@
|
|
|
1
1
|
module Kernel
|
|
2
2
|
|
|
3
3
|
# Deep clone an object by deep cloning every instance
|
|
4
|
-
# variable as well
|
|
4
|
+
# variable as well, along with the elements of an Array and
|
|
5
|
+
# the values of a Hash.
|
|
6
|
+
#
|
|
7
|
+
# a = [1, [2, [3]]]
|
|
8
|
+
# b = a.deep_clone
|
|
9
|
+
# b[1][1] << 4
|
|
10
|
+
# a #=> [1, [2, [3]]]
|
|
11
|
+
#
|
|
12
|
+
# Hash keys are not copied: Ruby already copies and freezes String
|
|
13
|
+
# keys, and a hash compared by identity needs the same key objects.
|
|
14
|
+
# Other containers, such as Struct and Set, share their members with
|
|
15
|
+
# the original.
|
|
16
|
+
#
|
|
17
|
+
# An object that is referenced more than once, including an object
|
|
18
|
+
# that contains itself, is copied once.
|
|
5
19
|
#
|
|
6
20
|
# Returns [Object]
|
|
7
21
|
|
|
8
|
-
def deep_clone(cache={})
|
|
22
|
+
def deep_clone(cache={}.compare_by_identity)
|
|
9
23
|
return cache[self] if cache.key?(self)
|
|
10
24
|
|
|
11
|
-
|
|
25
|
+
collection = (Array === self || Hash === self)
|
|
26
|
+
|
|
27
|
+
# A frozen collection's copy is filled in first, then frozen.
|
|
28
|
+
copy = collection ? clone(freeze: false) : clone
|
|
12
29
|
cache[self] = copy
|
|
13
30
|
|
|
14
|
-
|
|
15
|
-
val = instance_variable_get(var)
|
|
31
|
+
deep = lambda do |val|
|
|
16
32
|
begin
|
|
17
|
-
val
|
|
33
|
+
val.deep_clone(cache)
|
|
18
34
|
rescue TypeError
|
|
19
|
-
|
|
35
|
+
val
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
copy.instance_variables.each do |var|
|
|
40
|
+
copy.instance_variable_set(var, deep[instance_variable_get(var)])
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
if collection
|
|
44
|
+
if Array === copy
|
|
45
|
+
copy.map!(&deep)
|
|
46
|
+
else
|
|
47
|
+
copy.transform_values!(&deep)
|
|
20
48
|
end
|
|
21
|
-
copy.
|
|
49
|
+
copy.freeze if frozen?
|
|
22
50
|
end
|
|
23
51
|
|
|
24
52
|
return copy
|
|
@@ -2,10 +2,15 @@ class Module
|
|
|
2
2
|
|
|
3
3
|
# Alias a class method.
|
|
4
4
|
#
|
|
5
|
+
# class Greeter
|
|
6
|
+
# def self.hello; "Hello"; end
|
|
7
|
+
# alias_class_method :hi, :hello
|
|
8
|
+
# end
|
|
9
|
+
#
|
|
10
|
+
# Greeter.hi #=> "Hello"
|
|
11
|
+
#
|
|
5
12
|
def alias_class_method(name, target)
|
|
6
|
-
|
|
7
|
-
alias_method(name, target)
|
|
8
|
-
end
|
|
13
|
+
singleton_class.alias_method(name, target)
|
|
9
14
|
end
|
|
10
15
|
|
|
11
16
|
end
|
|
@@ -16,16 +16,5 @@ class Module
|
|
|
16
16
|
module_eval { define_method name, &blk }
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
#--
|
|
20
|
-
# TODO: Why are we making these protected,
|
|
21
|
-
# and where should these really go in Facets?
|
|
22
|
-
#++
|
|
23
|
-
protected :attr
|
|
24
|
-
protected :attr_reader
|
|
25
|
-
protected :attr_writer
|
|
26
|
-
protected :attr_accessor
|
|
27
|
-
protected :remove_method
|
|
28
|
-
protected :undef_method
|
|
29
|
-
|
|
30
19
|
end
|
|
31
20
|
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
class Module
|
|
2
2
|
|
|
3
|
-
private
|
|
4
|
-
|
|
5
3
|
# Creates a new method for a pre-existing method.
|
|
6
4
|
#
|
|
7
5
|
# If _aka_ is given, then the method being redefined will
|
|
@@ -21,14 +19,25 @@ class Module
|
|
|
21
19
|
#
|
|
22
20
|
# Greeter.new.hello #=> "Hello, friend!"
|
|
23
21
|
#
|
|
22
|
+
# The new method keeps the visibility of the one it replaces. Like
|
|
23
|
+
# ActiveSupport's Module#redefine_method, this is public.
|
|
24
|
+
#
|
|
24
25
|
# CREDIT: Trans
|
|
25
26
|
|
|
26
27
|
def redefine_method(sym, aka=nil, &blk)
|
|
28
|
+
visibility = if private_method_defined?(sym)
|
|
29
|
+
:private
|
|
30
|
+
elsif protected_method_defined?(sym)
|
|
31
|
+
:protected
|
|
32
|
+
else
|
|
33
|
+
:public
|
|
34
|
+
end
|
|
27
35
|
alias_method(aka, sym) if aka
|
|
28
36
|
## prevent warning about method overwrite
|
|
29
37
|
begin remove_method(sym) rescue NameError end
|
|
30
|
-
## define
|
|
38
|
+
## define replacement method
|
|
31
39
|
define_method(sym, &blk)
|
|
40
|
+
__send__(visibility, sym)
|
|
32
41
|
end
|
|
33
42
|
|
|
34
43
|
#
|
data/lib/core/facets/version.rb
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
# @deprecated Use
|
|
2
|
-
# initialize_copy method.
|
|
1
|
+
# @deprecated Use Kernel#deep_clone (`require 'facets/kernel/deep_clone'`)
|
|
2
|
+
# instead, or define your own initialize_copy method.
|
|
3
3
|
# Scheduled for removal after 2027-09-30.
|
|
4
|
-
warn "facets/cloneable is deprecated. Use
|
|
4
|
+
warn "facets/cloneable is deprecated. Use Kernel#deep_clone (require 'facets/kernel/deep_clone') " \
|
|
5
|
+
"or define your own initialize_copy instead. It will be removed after 2027-09-30.", uplevel: 1
|
|
5
6
|
|
|
6
7
|
module Cloneable
|
|
7
8
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
require 'find'
|
|
2
|
+
|
|
1
3
|
module Find
|
|
2
4
|
# Identical to find except select returns the matching files as an array.
|
|
3
5
|
# (find returns nil, which is not very useful if you actually wanted an array.)
|
|
@@ -8,7 +10,7 @@ module Find
|
|
|
8
10
|
#
|
|
9
11
|
# CREDIT: Tyler Rick
|
|
10
12
|
def self.select(*paths, &block)
|
|
11
|
-
|
|
13
|
+
find(*paths).select(&block)
|
|
12
14
|
end
|
|
13
15
|
end
|
|
14
16
|
|
|
@@ -16,19 +16,24 @@ module Math
|
|
|
16
16
|
# require 'facets/math/percentile'
|
|
17
17
|
#
|
|
18
18
|
def self.percentile(array, pcnt)
|
|
19
|
-
sorted_array = array.sort
|
|
20
|
-
|
|
21
19
|
return nil if array.length == 0
|
|
22
20
|
|
|
21
|
+
sorted_array = array.sort
|
|
22
|
+
|
|
23
23
|
rank = (pcnt.to_f / 100) * (array.length + 1)
|
|
24
24
|
whole = rank.truncate
|
|
25
|
-
|
|
25
|
+
|
|
26
|
+
# A rank below 1 or past the last value is clamped to the minimum or
|
|
27
|
+
# maximum, as NIST specifies.
|
|
28
|
+
return sorted_array.first if whole < 1
|
|
29
|
+
return sorted_array.last if whole >= array.length
|
|
30
|
+
|
|
26
31
|
# if has fractional part
|
|
27
32
|
if whole != rank
|
|
28
33
|
s0 = sorted_array[whole - 1]
|
|
29
34
|
s1 = sorted_array[whole]
|
|
30
35
|
|
|
31
|
-
f =
|
|
36
|
+
f = rank - whole
|
|
32
37
|
|
|
33
38
|
return (f * (s1 - s0)) + s0
|
|
34
39
|
else
|
|
@@ -4,21 +4,17 @@ module Shellwords
|
|
|
4
4
|
|
|
5
5
|
module_function
|
|
6
6
|
|
|
7
|
-
# Escape
|
|
8
|
-
#
|
|
7
|
+
# Escape backslash, tab, space, and the characters | & ` < > ( ) ' "
|
|
8
|
+
# with a backslash.
|
|
9
9
|
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
10
|
+
# Unlike Ruby's Shellwords.escape, this leaves other shell
|
|
11
|
+
# metacharacters alone, including $ (so shell variables still
|
|
12
|
+
# expand), ; * ? ~ and newlines. It is not safe for untrusted
|
|
13
|
+
# input: use Shellwords.escape for that.
|
|
12
14
|
def alt_escape(cmdline)
|
|
13
15
|
cmdline.gsub(/([\\\t\| &`<>)('"])/) { |s| '\\' << s }
|
|
14
16
|
end
|
|
15
17
|
|
|
16
|
-
unless method_defined?(:escape)
|
|
17
|
-
def escape(cmdline)
|
|
18
|
-
cmdline.gsub(/([\\\t\| &`<>)('"])\$/) { |s| '\\' << s }
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
|
|
22
18
|
# Escape special character used in DOS-based shells.
|
|
23
19
|
#
|
|
24
20
|
# TODO: How to integrate with rest of system?
|