These refinements augment and enhance Ruby primitives so you can avoid monkey patches. They also allow you to develop clean and concise implementations while using less code. By refining your code, you can acquire the functionality you wish the core primitives had!
- Features
- Requirements
- Setup
- Usage
- Requires
- Using
- Examples
- Array
- Binding
- Data
- DateTime
- Hash
- .infinite
- .with_default
- #compress
- #compress!
- #deep_merge
- #deep_merge!
- #deep_stringify_keys
- #deep_stringify_keys!
- #deep_symbolize_keys
- #deep_symbolize_keys!
- #diff
- #fetch_value
- #flatten_keys
- #flatten_keys!
- #many?
- #recurse
- #stringify_keys
- #stringify_keys!
- #symbolize_keys
- #symbolize_keys!
- #transform_value
- #transform_value!
- #transform_with
- #transform_with!
- #use
- IO
- LogDevice
- Logger
- Module
- Object
- Pathname
- String
- String IO
- Struct
- Symbol
- Development
- Tests
- License
- Security
- Code of Conduct
- Contributions
- Developer Certificate of Origin
- Versions
- Community
- Credits
Features
Enhances the following objects:
-
Array
-
Binding
-
Data
-
DateTime
-
Hash
-
IO
-
LogDevice
-
Logger
-
Module
-
Object
-
Pathname
-
String
-
StringIO
-
Struct
-
Symbol
Requirements
-
Ruby.
-
A solid understanding of refinements.
Setup
To install with security, run:
# 💡 Skip this line if you already have the public certificate installed.
gem cert --add <(curl --compressed --location https://alchemists.io/gems.pem)
gem install refinements --trust-policy HighSecurity
To install without security, run:
gem install refinements
You can also add the gem directly to your project:
bundle add refinements
Once the gem is installed, you only need to require it:
require "refinements"
Usage
Requires
If no refinements are desired, then add the following to your Gemfile
instead:
gem "refinements", require: false
…then require the specific refinement, as needed. Example:
require "refinements/array"
require "refinements/binding"
require "refinements/data"
require "refinements/date_time"
require "refinements/hash"
require "refinements/io"
require "refinements/log_device"
require "refinements/logger"
require "refinements/module"
require "refinements/object"
require "refinements/pathname"
require "refinements/string"
require "refinements/string_io"
require "refinements/struct"
require "refinements/symbol"
Using
Much like including/extending a module, you’ll need to modify your object(s) to use the refinement(s):
class Example
using Refinements::Array
using Refinements::Binding
using Refinements::Data
using Refinements::DateTime
using Refinements::Hash
using Refinements::IO
using Refinements::LogDevice
using Refinements::Logger
using Refinements::Module
using Refinements::Object
using Refinements::Pathname
using Refinements::String
using Refinements::StringIO
using Refinements::Struct
using Refinements::Symbol
end
Examples
The following sections demonstrate how each refinement enriches your objects with new capabilities.
Array
#combinatorial?
Answers if an array is equal to another array when the elements are equal but in any order and/or subset.
example = %w[a b c]
example.combinatorial? %w[a b c] # true
example.combinatorial? %w[c a b] # true
example.combinatorial? %w[c] # true
example.combinatorial? %w[c b] # true
example.combinatorial? %w[x] # false
example.combinatorial? %w[z b c] # false
example.combinatorial? %w[a b c d] # false
example.combinatorial? [] # false
#compress
Removes nil
and empty objects without mutating itself. Answers itself if there is nothing to remove.
object = Object.new
example = [1, "blueberry", nil, "", [], {}, object]
[].compress # []
[1, 2].compress # [1, 2]
example.compress # [1, "blueberry", object]
example # [1, "blueberry", nil, "", [], {}, object]
#compress!
Removes nil
and empty values while mutating itself. Answers nil
if there is nothing to remove.
object = Object.new
example = [1, "blueberry", nil, "", [], {}, object]
[].compress! # nil
[1, 2].compress! # nil
example.compress! # [1, "blueberry", object]
example # [1, "blueberry", object]
#excluding
Removes given array or elements without mutating itself.
[1, 2, 3, 4, 5].excluding [4, 5] # [1, 2, 3]
[1, 2, 3, 4, 5].excluding 4, 5 # [1, 2, 3]
#filter_find
⚠️ This is deprecated and will be removed in Version 13.0.0.
Answers the first element which evaluates to true from a filtered collection.
handlers = [
-> object { object if object == :b },
proc { false },
-> object { object if object == :a }
]
handlers.filter_find # Enumerator::Lazy
handlers.filter_find { |handler| handler.call :a } # :a
handlers.filter_find { |handler| handler.call :x } # nil
#including
Adds given array or elements without mutating itself.
[1, 2, 3].including [4, 5] # [1, 2, 3, 4, 5]
[1, 2, 3].including 4, 5 # [1, 2, 3, 4, 5]
#intersperse
Inserts additional elements, or an array, between all members of given array.
[1, 2, 3].intersperse :a # [1, :a, 2, :a, 3]
[1, 2, 3].intersperse :a, :b # [1, :a, :b, 2, :a, :b, 3]
[1, 2, 3].intersperse %i[a b c] # [1, :a, :b, :c, 2, :a, :b, :c, 3]
#many?
Answers true if an array has more than one element. Can take a block which evaluates as truthy or falsey.
[1, 2].many? # true
[1, 2, 3].many?(&:odd?) # true
[1].many? # false
[].many? # false
#maximum
Answers the maximum extracted value from a collection of objects.
Point = Data.define :x, :y
points = [Point[x: 1, y: 2], Point[x: 0, y: 1], Point[x: 2, y: 3]]
points.maximum :x # 2
points.maximum :y # 3
points.maximum :z # undefined method `z' for #<data Point x=1, y=2> (NoMethodError)
[].maximum :x # nil
#mean
Answers mean/average all elements within an array.
[].mean # 0
[5].mean # 5
[1, 2, 3].mean # 2
[1.25, 1.5, 1.75].mean # 1.5
#minimum
Answers the minimum extracted value from a collection of objects.
Point = Data.define :x, :y
points = [Point[x: 1, y: 2], Point[x: 0, y: 1], Point[x: 2, y: 3]]
points.minimum :x # 0
points.minimum :y # 1
points.minimum :z # undefined method `z' for #<data Point x=1, y=2> (NoMethodError)
[].minimum :x # nil
#pad
Answers new array padded with given value up to a maximum size. Useful in situations where an array needs to be a specific size with padded values.
["a"].pad 0 # ["a"]
["a"].pad "-", 3 # ["a", "-", "-"]
%w[a b].pad "-", 3 # ["a", "b", "-"]
#pick
Answers value of first element that matches given key.
array = [{name: "a", label: "A"}, {name: "b", label: "B"}, {name: "c", label: "C"}]
array.pick :name # "a"
array.pick :name, :label # ["a", "A"]
array.pick # nil
[].pick :other # nil
#pluck
Answers values of all elements that match given keys.
array = [{name: "a", label: "A"}, {name: "b", label: "B"}, {name: "c", label: "C"}]
array.pluck :name # ["a", "b", "c"]
array.pluck :name, :label # [["a", "A"], ["b", "B"], ["c", "C"]]
array.pluck # []
[].pluck :other # []
#replace_at
Answers mutated array where an element — at a specific index — is replaced by single or multiple elements.
%i[a b c].replace_at 0, :x # [:x, :b, :c]
%i[a b c].replace_at 1, :x # [:a, :x, :c]
%i[a b c].replace_at 1, :x, :y # [:a, :x, :y, :c]
%i[a b c].replace_at -1, :x # [:a, :b, :x]
#ring
Answers a circular array which can enumerate before, current, after elements.
example = [1, 2, 3]
example.ring # "#<Enumerator: ...>"
example.ring { |(before, current, after)| puts "#{before} #{current} #{after}" }
# [3 1 2]
# [1 2 3]
# [2 3 1]
#supplant
Answers mutated array where first target element found is replaced by single or multiple elements.
%i[a b a].supplant :a, :z # [:z, :b, :a]
%i[a b a].supplant :a, :z, :y # [:z, :y, :b, :a]
%i[a b a].supplant :a, %i[z y] # [[:z, :y], :b, :a]
#supplant_if
Answers mutated array where all target elements are replaced by single or multiple elements.
⚠️ Be aware that this can be an expensive operation on large arrays.
%i[a b a].supplant_if :a, :z # [:z, :b, :z]
%i[a b a].supplant_if :a, :z, :y # [:z, :y, :b, :z, :y]
%i[a b a].supplant_if :a, %i[z y] # [[:z, :y], :b, [:z, :y]]
#to_sentence
Answers a sentence using "and"
as the default conjunction and ", "
as the default delimiter.
Useful when building documentation, answering human readable error messages, etc.
[].to_sentence # ""
["demo"].to_sentence # "demo"
["a", :b].to_sentence # "a and b"
[1, "a", :b, 2.0, /\w+/].to_sentence # "1, a, b, 2.0, and (?-mix:\\w+)"
%w[one two three].to_sentence # "one, two, and three"
%w[eins zwei drei].to_sentence "und", delimiter: " " # "eins zwei und drei"
💡 You can use a string or a symbol for the conjunction (i.e. "and"
or :and
).
#to_usage
Further enhances #to_sentence
by answering a sentence where all elements are inspected (i.e. #inspect
) before turned into a sentence using "and"
as the default conjunction and ", "
as the default delimiter. This is useful when providing detailed error messages and you need the types of all elements preserved.
[].to_usage # ""
["demo"].to_usage # "\"demo\""
["a", :b].to_usage # "\"a\" and :b"
[1, "a", :b, 2.0, /\w+/].to_usage # "1, \"a\", :b, 2.0, and /\\w+/"
%w[one two three].to_usage # "\"one\", \"two\", and \"three\""
%w[eins zwei drei].to_usage "und", delimiter: " " # "\"eins\" \"zwei\" und \"drei\""
💡 You can use a string or a symbol for the conjunction (i.e. "and"
or :and
).
Binding
#[]
Allows you to obtain a local variable. This is an alias to #local_variable_get
.
a = 1
binding[:a] # 1
binding[:bogus] # `bogus' is not defined (NameError)
#[]=
Allows you to set a local variable. This is an alias to #local_variable_set
.
a = 1
binding[:a] = 5
binding[:bogus] = "bad"
binding[:a] # 5
binding[:bogus] # # `bogus' is not defined (NameError)
#local?
Allows you to check if local variable is defined. This is an alias to #local_variable_defined?
.
a = 1
binding.local? :a # true
binding.local? :b # false
#locals
Allows you to acquire all locally defined variables. This is an alias to #local_variables
.
binding.locals # []
a = 1
b = 2
binding.locals # [:a, :b]
Data
#diff
Allows you to obtain the differences between two objects.
implementation = Data.define :a, :b, :c
one = implementation.new a: 1, b: 2, c: 3
two = implementation.new a: 3, b: 2, c: 1
three = Data.define(:x, :y).new x: 1, y: 2
one.diff one # {}
one.diff two # {:a=>[1, 3], :c=>[3, 1]}
one.diff three # {:a=>[1, nil], :b=>[2, nil], :c=>[3, nil]}
one.diff Object.new # {:a=>[1, nil], :b=>[2, nil], :c=>[3, nil]}
Any object that is not the same type will have a nil
value as shown in the last two examples.
DateTime
.utc
Answers new DateTime object for current UTC date/time.
DateTime.utc # "#<DateTime: 2019-12-31T18:17:00+00:00 ((2458849j,65820s,181867000n),+0s,2299161j)>"
Hash
.infinite
Answers new hash where missing keys, even deeply nested, answer an empty hash.
example = Hash.infinite
example[:a] # {}
example[:a][:b][:c] # {}
.with_default
Answers new hash where every top-level missing key has the same default value.
example = Hash.with_default ""
example[:a] # ""
example = Hash.with_default []
example[:b] # []
#compress
Removes nil
and empty objects without mutating itself. Answers itself if nothing to remove.
object = Object.new
example = {a: 1, b: "blueberry", c: nil, d: "", e: [], f: {}, g: object}
{}.compress # {}
{a: 1, b: 2}.compress # {a: 1, b: 2}
example.compress # {a: 1, b: "blueberry", g: object}
example # {a: 1, b: "blueberry", c: nil, d: "", e: [], f: {}, g: object}
#compress!
Removes nil
and empty objects while mutating itself. Answers nil
if nothing to remove.
object = Object.new
example = {a: 1, b: "blueberry", c: nil, d: "", e: [], f: {}, g: object}
{}.compress! # nil
{a: 1, b: 2}.compress! # nil
example.compress! # {a: 1, b: "blueberry", g: object}
example # {a: 1, b: "blueberry", g: object}
#deep_merge
Merges deeply nested hashes together without mutating itself.
example = {a: "A", b: {one: "One", two: "Two"}}
example.deep_merge b: {one: 1} # {a: "A", b: {one: 1, two: "Two"}}
example # {a: "A", b: {one: "One", two: "Two"}}
#deep_merge!
Merges deeply nested hashes together while mutating itself.
example = {a: "A", b: {one: "One", two: "Two"}}
example.deep_merge! b: {one: 1} # {a: "A", b: {one: 1, two: "Two"}}
example # {a: "A", b: {one: 1, two: "Two"}}
#deep_stringify_keys
Answers string keys of a nested hash without mutating itself. Does not handle nested arrays, though.
example = {a: {b: 2}}
example.deep_stringify_keys # {"a" => {"b" => 1}}
example # {a: {b: 2}}
#deep_stringify_keys!
Answers string keys of nested hash while mutating itself. Does not handle nested arrays, though.
example = {a: {b: 2}}
example.deep_stringify_keys! # {"a" => {"b" => 1}}
example # {"a" => {"b" => 1}}
#deep_symbolize_keys
Symbolizes keys of nested hash without mutating itself. Does not handle nested arrays, though.
example = {"a" => {"b" => 2}}
example.deep_symbolize_keys # {a: {b: 1}}
example # {"a" => {"b" => 2}}
#deep_symbolize_keys!
Symbolizes keys of nested hash while mutating itself. Does not handle nested arrays, though.
example = {"a" => {"b" => 2}}
example.deep_symbolize_keys! # {a: {b: 1}}
example # {a: {b: 1}}
#diff
Allows you to obtain the differences between two objects.
one = {a: 1, b: 2, c: 3}
two = {a: 3, b: 2, c: 1}
three = {c: 3, b: 2, a: 1}
four = Data.define(:x, :y).new x: 1, y: 2
one.diff one # {}
one.diff two # {:a=>[1, 3], :c=>[3, 1]}
one.diff three # {}
one.diff four # {:a=>[1, nil], :b=>[2, nil], :c=>[3, nil]}
one.diff Object.new # {:a=>[1, nil], :b=>[2, nil], :c=>[3, nil]}
Any object that is not the same type will have a nil
value as shown in the last two examples. Two hashes with the same keys but defined in different order behave as if they had the same key order.
#fetch_value
Fetches value for exiting or missing key. Behavior is identical to #fetch
except when the value of
the key is nil
you’ll get the default value instead. This eliminates the need for using an or
expression: example.fetch(:desired_key) || "default_value"
.
{a: "demo"}.fetch_value :a, "default" # "demo"
{a: "demo"}.fetch_value :a # "demo"
{a: nil}.fetch_value :a, "default" # "default"
{a: nil}.fetch_value(:a) { "default" } # "default"
{}.fetch_value :a # KeyError
{}.fetch_value(:a) { "default" } # "default"
{a: "demo"}.fetch_value # ArgumentError
#flatten_keys
Flattens nested keys as top-level keys without mutating itself. Keys are converted to symbols. Does not handle nested arrays.
{a: {b: 1}}.flatten_keys prefix: :demo # {demo_a_b: 1}
{a: {b: 1}}.flatten_keys delimiter: :| # {:"a|b" => 1}
example = {a: {b: 1}}
example.flatten_keys # {a_b: 1}
example # {a: {b: 1}}
#flatten_keys!
Flattens nested keys as top-level keys while mutating itself. Keys are converted to symbols. Does not handle nested arrays.
{a: {b: 1}}.flatten_keys! prefix: :demo # {demo_a_b: 1}
{a: {b: 1}}.flatten_keys! delimiter: :| # {:"a|b" => 1}
example = {a: {b: 1}}
example.flatten_keys! # {a_b: 1}
example # {a_b: 1}
#many?
Answers true if a hash has more than one element. Can take a block which evaluates as truthy or falsey.
{a: 1, b: 2}.many? # true
{a: 1, b: 2, c: 2}.many? { |_key, value| value == 2 } # true
{a: 1}.many? # false
{}.many? # false
#recurse
Recursively iterates over the hash and any hash value by applying the given block to it. Does not handle nested arrays, though.
example = {"a" => {"b" => 1}}
example.recurse(&:symbolize_keys) # {a: {b: 1}}
example.recurse(&:invert) # {{"b" => 1} => "a"}
#stringify_keys
Converts keys to strings without mutating itself.
example = {a: 1, b: 2}
example.stringify_keys # {"a" => 1, "b" => 2}
example # {a: 1, b: 2}
#stringify_keys!
Converts keys to strings while mutating itself.
example = {a: 1, b: 2}
example.stringify_keys! # {"a" => 1, "b" => 2}
example # {"a" => 1, "b" => 2}
#symbolize_keys
Converts keys to symbols without mutating itself.
example = {"a" => 1, "b" => 2}
example.symbolize_keys # {a: 1, b: 2}
example # {"a" => 1, "b" => 2}
#symbolize_keys!
Converts keys to symbols while mutating itself.
example = {"a" => 1, "b" => 2}
example.symbolize_keys! # {a: 1, b: 2}
example # {a: 1, b: 2}
#transform_value
Transforms a value for the specified key only if the key exists and a block is given. Otherwise, the original hash is answered. Does not mutate itself.
example = {a: 1, b: 2}
example.transform_value :b # {a: 1, b: 2}
example.transform_value(:b) { 20 } # {a: 1, b: 20}
example.transform_value(:b) { |value| value * 10 } # {a: 1, b: 20}
example.transform_value :c # {a: 1, b: 2}
example.transform_value(:c) { :bogus } # {a: 1, b: 2}
The original object is not mutated:
example.transform_value(:b) { 20 } # {a: 1, b: 20}
example # {a: 1, b: 2}
#transform_value!
Transforms a value for the specified key only if the key exists and a block is given. Otherwise, the original hash is answered. Mutates itself.
example = {a: 1, b: 2}
example.transform_value! :b # {a: 1, b: 2}
example.transform_value!(:b) { 20 } # {a: 1, b: 20}
example.transform_value!(:b) { |value| value * 10 } # {a: 1, b: 20}
example.transform_value! :c # {a: 1, b: 2}
example.transform_value!(:c) { :bogus } # {a: 1, b: 2}
The original object is mutated:
example.transform_value!(:b) { 20 } # {a: 1, b: 20}
example # {a: 1, b: 20}
#transform_with
Transforms values of keys using specific operations (i.e. any object that responds to #call
). Does not mutate itself and you can transform multiple values at once:
example = {name: "Jayne Doe", email: "<[email protected]>"}
example.transform_with name: -> value { value.delete_suffix " Doe" },
email: -> value { value.tr "<>", "" }
# {name: "Jayne", email: "[email protected]"}
Invalid keys are ignored:
example.transform_with bogus: -> value { value.tr "<>", "" }
# {email: "<[email protected]>"}
The original object is not mutated:
example # {name: "Jayne Doe", email: "<[email protected]>"}
#transform_with!
Transforms values of keys using specific operations (i.e. any object that responds to #call
). Mutates itself and you can transform multiple values at once:
example = {name: "Jayne Doe", email: "<[email protected]>"}
example.transform_with! name: -> value { value.delete_suffix " Doe" },
email: -> value { value.tr "<>", "" }
# {name: "Jayne", email: "[email protected]"}
Invalid keys are ignored:
example.transform_with! bogus: -> value { value.tr "<>", "" }
# {email: "<[email protected]>"}
The original object is mutated:
example # {name: "Jayne", email: "[email protected]"}
#use
Uses the hash’s keys as block arguments where the value of the block argument is equal to the value of the key found within the hash. Works best with hashes that use symbols for keys but falls back to string keys when symbol keys can’t be found.
example = {unit: "221B", street: "Baker Street", city: "London", country: "UK"}
example.use { |unit, street| "#{unit} #{street}" } # "221B Baker Street"
IO
.void
Answers an IO stream which points to /dev/null
in order to ignore any reads or writes to the
stream. When given a block, the stream will automatically close upon block exit. When not given a
block, you’ll need to close the stream manually.
io = IO.void # "#<IO:fd 20>"
io = IO.void { |void| void.write "nevermore" } # "#<IO:(closed)>"
#redirect
Redirects current stream to other stream when given a block. Without a block, the original stream is answered instead.
io = IO.new IO.sysopen(Pathname("demo.txt").to_s, "w+")
other = IO.new IO.sysopen(Pathname("other.txt").to_s, "w+")
io.redirect other # "#<IO:fd 20>"
io.redirect(other) { |stream| stream.write "demo" } # "#<IO:fd 21>"
#reread
Answers full stream by rewinding to beginning of stream and reading all content.
io = IO.new IO.sysopen(Pathname("demo.txt").to_s, "w+")
io.write "This is a demo."
io.reread # "This is a demo."
io.reread 4 # "This"
buffer = "".dup
io.reread(buffer:) # "This is a demo."
buffer # "This is a demo."
#squelch
Temporarily ignores any reads/writes for code executed within a block. Answers itself without any arguments or when given a block.
io = IO.new IO.sysopen(Pathname("test.txt").to_s, "w+")
io.squelch # "#<IO:fd 20>"
io.squelch { io.write "Test" } # "#<IO:fd 20>"
io.reread # ""
LogDevice
#reread
Answers previously written content by rewinding to beginning of device.
# With File.
device = Logger::LogDevice.new "test.log"
device.write "Test."
device.reread # "Test."
# With StringIO.
device = Logger::LogDevice.new StringIO.new
device.write "Test."
device.reread # "Test."
# With STDOUT.
device = Logger::LogDevice.new $stdout
device.write "Test."
device.reread # ""
Logger
#reread
Answers previously written content by rewinding to beginning of log.
# With File.
logger = Logger.new "test.log"
logger.write "Test."
logger.reread # "Test."
# With StringIO.
logger = Logger.new StringIO.new
logger.write "Test."
logger.reread # "Test."
# With STDOUT.
logger = Logger.new $stdout
logger.write "Test."
logger.reread # ""
#any
Allows you to log any message which is identical in behavior and functionality to the Logger#unknown
method only this requires less typing and better matches the terminology used by the #unknown
method.
logger = Logger.new STDOUT
logger.any "Test."
# A, [2000-01-10T09:00:00.847428 #44925] ANY -- : Test.
logger.any { "Test." }
A, [2000-01-10T09:00:00.330719 #44925] ANY -- : Test.
Module
#pseudonym
Allows you to set a temporary name for your anonymous Module
(or Class
since Class
inherits from Module
) with a better default than what #set_temporary_name
provides.
Module.new.pseudonym "demo" # demo-44600
Module.new.pseudonym "demo", delimiter: "_" # demo_60900
Module.new.pseudonym "demo", nil # demo-
Module.new.pseudonym "demo", nil, delimiter: nil # demo
The same applies for anonymous classes since classes inherit from modules:
Class.new.pseudonym "demo" # demo-44600 < Object
Class.new.pseudonym "demo", delimiter: "_" # demo_60900 < Object
Class.new.pseudonym "demo", nil # demo- < Object
Class.new.pseudonym "demo", nil, delimiter: nil # demo < Object
💡 While convenient, if you find yourself nullifying the suffix and/or delimiter, you’re better off using #set_temporary_name
.
Object
#in?
Allows you to know if self
is included in, or an element of, the target object.
1.in? [1, 2, 3] # true
9.in? [1, 2, 3] # false
"a".in? %w[a b c] # true
"z".in? %w[a b c] # false
:a.in? %i[a b c] # true
:z.in? %i[a b c] # false
:a.in? %i[a b c].to_enum # true
:z.in? %i[a b c].to_enum # false
:a.in?({a: 1, b: 2, c: 3}) # true
:z.in?({a: 1, b: 2, c: 3}) # false
1.in? 1..5 # true
9.in? 1..5 # false
1.in? Set[1, 2, 3] # true
9.in? Set[1, 2, 3] # false
"a".in? "abcde" # true
"z".in? "abcde" # false
"z".in? Object.new # `String#include?` must be implemented. (NoMethodError)
#to_proc
Allows you to cast any object to a proc.
one = Class.new { def call = :test }
.new
two = Object.new
one.to_proc # #<Proc:0x0000000124019580 (lambda)>
two.to_proc # `Object#call` must be implemented. (NoMethodError)
Pathname
Pathname
Enhances the Kernel
conversion function which casts nil
into a pathname in order to avoid:
TypeError (no implicit conversion of nil into String)
. The pathname remains invalid but at least
you have an instance of Pathname
, which behaves like a Null Object, that can be used to
construct a valid path.
Pathname nil # Pathname("")
.home
Answers user home directory.
Pathname.home # Pathname "/Users/demo"
.make_temp_dir
Wraps Dir.mktmpdir
with the following behavior (see
Dir.mktmpdir for details):
-
Without Block - Answers a newly created Pathname instance which is not automatically cleaned up.
-
With Block Yields a Pathname instance, answers result of given block, and automatically cleans up temporary directory after block exits.
The following examples use truncated temporary directories for illustration purposes only. In reality, these paths will be longer depending on which operating system you are using.
Pathname.make_temp_dir # Pathname:/var/folders/T/temp-20200101-16940-r8
Pathname.make_temp_dir prefix: "prefix-" # Pathname:/var/folders/T/prefix-20200101-16940-r8
Pathname.make_temp_dir suffix: "-suffix" # Pathname:/var/folders/T/temp-20200101-16940-r8-suffix
Pathname.make_temp_dir prefix: "prefix-", suffix: "-suffix" # Pathname:/var/folders/T/prefix-20200101-16940-r8-suffix
Pathname.make_temp_dir root: "/example" # Pathname:/example/temp-20200101-16940-r8
Pathname.make_temp_dir { "I am a block result" } # "I am a block result"
Pathname.make_temp_dir { |path| path.join "sub_dir" } # Pathname:/var/folders/T/temp-20200101-16940-r8/sub_dir
.require_tree
Requires all Ruby files in given root path and corresponding nested tree structure. All files are sorted before being required to ensure consistent behavior. Example:
# Before
Dir[File.join(__dir__, "support/shared_contexts/**/*.rb")].sort.each { |path| require path }
# After
Pathname.require_tree "#{__dir__}/support/shared_contexts"
The following are further examples of potential usage:
# Requires all files in root directory and below.
Pathname.require_tree __dir__
# Requires all files in `/test/**/*.rb` and below.
Pathname.require_tree "/test"
# Requires all files in RSpec shared examples directory structure.
Pathname.require_tree SPEC_ROOT.join("support/shared_examples")
.root
Answers operating system root path.
Pathname.root # Pathname "/"
#change_dir
Wraps Dir.chdir
behavior by changing to directory of current path. See
Dir.chdir for details.
current = Pathname.pwd # "$HOME/demo" (Present Working Directory)
custom = current.join("test").make_dir # Pathname "$HOME/demo/test"
custom.change_dir # "$HOME/demo/test" (Present Working Directory)
current.change_dir # "$HOME/demo" (Present Working Directory)
custom.change_dir { "example" } # "example"
custom.change_dir { |path| path } # Pathname "$HOME/demo/test"
Pathname.pwd # "$HOME/demo" (Present Working Directory)
#copy
Copies file from current location to new location while answering itself so it can be chained.
Pathname("input.txt").copy Pathname("output.txt") # Pathname("input.txt")
#deep_touch
Has all of the same functionality as the #touch
method while being able to create ancestor
directories no matter how deeply nested the file might be.
Pathname("a/b/c/d.txt").touch # Pathname("a/b/c/d.txt")
Pathname("a/b/c/d.txt").touch Time.now - 1 # Pathname("a/b/c/d.txt")
#delete
Deletes file or directory and answers itself so it can be chained.
# When path exists.
Pathname("/example.txt").touch.delete # Pathname("/example")
# When path doesn't exist.
Pathname("/example.txt").delete # Errno::ENOENT
#delete_prefix
Deletes a path prefix and answers new pathname.
Pathname("a/path/example-test.rb").delete_prefix "example-" # Pathname("a/path/test.rb")
Pathname("example-test.rb").delete_prefix "example-" # Pathname("test.rb")
Pathname("example-test.rb").delete_prefix "miss" # Pathname("example-test.rb")
#delete_suffix
Deletes a path suffix and answers new pathname.
Pathname("a/path/test-example.rb").delete_suffix "-example" # Pathname("a/path/test.rb")
Pathname("test-example.rb").delete_suffix "-example" # Pathname("test.rb")
Pathname("test-example.rb").delete_suffix "miss" # Pathname("test-example.rb")
#directories
Answers all directories or filtered directories for current path.
Pathname("/example").directories # [Pathname("a"), Pathname("b")]
Pathname("/example").directories "a*" # [Pathname("a")]
Pathname("/example").directories flag: File::FNM_DOTMATCH # [Pathname(".."), Pathname(".")]
#empty
Empties a directory of children (i.e. folders, nested folders, or files) or clears an existing file of contents. If a directory or file doesn’t exist, it will be created.
directory = Pathname("test").make_path
file = directory.join("test.txt").write("example")
file.empty.read # ""
directory.empty.children # []
#extensions
Answers file extensions as an array.
Pathname("example.txt.erb").extensions # [".txt", ".erb"]
#files
Answers all files or filtered files for current path.
Pathname("/example").files # [Pathname("a.txt"), Pathname("a.png")]
Pathname("/example").files "*.png" # [Pathname("a.png")]
Pathname("/example").files flag: File::FNM_DOTMATCH # [Pathname(".ruby-version")]
#gsub
Same behavior as String#gsub
but answers a path with patterns replaced with desired substitutes.
Pathname("/a/path/some/path").gsub "path", "test"
# Pathname("/a/test/some/test")
Pathname("/%placeholder%/some/%placeholder%").gsub "%placeholder%", "test"
# Pathname("/test/some/test")
#make_ancestors
Ensures all ancestor directories are created for a path.
Pathname("/one/two").make_ancestors # Pathname("/one/two")
Pathname("/one").exist? # true
Pathname("/one/two").exist? # false
#make_dir
Provides alternative #mkdir
behavior by always answering itself (even when directory exists) and
not throwing errors when directory does exist in order to ensure the pathname can be chained.
Pathname("/one").make_dir # Pathname("/one")
Pathname("/one").make_dir.make_dir # Pathname("/one")
#make_path
Provides alternative #mkpath
behavior by always answering itself (even when full path exists) and
not throwing errors when directory does exist in order to ensure the pathname can be chained.
Pathname("/one/two/three").make_path # Pathname("/one/two/three")
Pathname("/one/two/three").make_path.make_path # Pathname("/one/two/three")
#name
Answers file name without extension.
Pathname("example.txt").name # Pathname("example")
#puts
Wraps #write
by writing content to file with new line and answering itself. Allows you to more easily swap out a Pathname
object with similar IO objects who support #puts
: IO
, StringIO
, File
, Kernel
, and so forth.
path = Pathname("test.txt").touch
path.puts "Test."
path.read # "Test.\n"
Pathname("text.txt").touch.puts("Test.").read # "Test.\n"
#relative_parent
Answers relative path from parent directory. This complements: #relative_path_from
.
Pathname("/one/two/three").relative_parent "/one" # Pathname "two"
#remove_dir
Provides alternative #rmdir
behavior by always answering itself (even when full path exists) and
not throwing errors when directory does exist in order to ensure the pathname can be chained.
Pathname("/test").make_dir.remove_dir.exist? # false
Pathname("/test").remove_dir # Pathname("/test")
Pathname("/test").remove_dir.remove_dir # Pathname("/test")
#remove_tree
Provides alternative #rmtree
behavior by always answering itself (even when full path exists) and
not throwing errors when directory does exist in order to ensure the pathname can be chained.
parent_path = Pathname "/one"
child_path = parent_path.join "two"
child_path.make_path
parent_path.remove_tree # Pathname "/one"
child_path.exist? # false
parent_path.exist? # false
child_path.make_path
child_path.remove_tree # Pathname "/one/two"
child_path.exist? # false
parent_path.exist? # true
#rewrite
When given a block, it provides the contents of the recently read file for manipulation and immediate writing back to the same file.
Pathname("/test.txt").rewrite # Pathname("/test.txt")
Pathname("/test.txt").rewrite { |body| body.sub "[token]", "example" } # Pathname("/test.txt")
#touch
Updates access and modification times for an existing path by defaulting to current time. When path doesn’t exist, it will be created as a file.
Pathname("example").touch # Pathname("example")
Pathname("example").touch Time.now - 1 # Pathname("example")
Pathname("example.txt").touch # Pathname("example.txt")
Pathname("example.txt").touch Time.now - 1 # Pathname("example.txt")
#write
Writes to file and answers itself so it can be chained. See IO.write
for details on additional
options.
Pathname("example.txt").write "test" # Pathname("example.txt")
Pathname("example.txt").write "test", offset: 1 # Pathname("example.txt")
Pathname("example.txt").write "test", mode: "a" # Pathname("example.txt")
String
#blank?
Answers true
/false
based on whether string is blank, <space>
, \n
, \t
, and/or \r
.
" \n\t\r".blank? # true
#camelcase
Answers a camel cased string.
"this_is_an_example".camelcase # "ThisIsAnExample"
#down
Answers string with only first letter down cased.
"EXAMPLE".down # "eXAMPLE"
#first
Answers first character of a string or first set of characters if given a number.
"example".first # "e"
"example".first 4 # "exam"
#indent
Answers indentation (string) which is the result of the multiplier times padding. By default, the multiplier is 1
and the padding is " "
which equates to two spaces.
"example".indent # " example"
"example".indent 0 # "example"
"example".indent -1 # "example"
"example".indent 2 # " example"
"example".indent 3, pad: " " # " example"
#last
Answers last character of a string or last set of characters if given a number.
"instant".last # "t"
"instant".last 3 # "ant"
#pluralize
Answers plural form of self when given a suffix to add. The plural form of the word can be dynamically calculated when given a count and a replacement pattern (i.e. string or regular expression) can be supplied for further specificity. Usage is based on plurals in English which may or may not work well in other languages.
"apple".pluralize "s" # apples
"apple".pluralize "s", 0 # apples
"apple".pluralize "s", 1 # apple
"apple".pluralize "s", -1 # apple
"apple".pluralize "s", 2 # apples
"apple".pluralize "s", -2 # apples
"cactus".pluralize "i", replace: "us" # cacti
"cul-de-sac".pluralize "ls", replace: "l" # culs-de-sac
#singularize
Answers singular form of self when given a suffix to remove (can be a string or a regular expression). The singular form of the word can be dynamically calculated when given a count and a replacement string can be supplied for further specificity. Usage is based on plurals in English which may or may not work well in other languages.
"apples".singularize "s" # apple
"sacks".singularize /s$/ # sack
"apples".singularize "s", 0 # apples
"apples".singularize "s", 1 # apple
"apples".singularize "s", -1 # apple
"apples".singularize "s", 2 # apples
"apples".singularize "s", -2 # apples
"cacti".singularize "i", replace: "us" # cactus
"culs-de-sac".singularize "ls", replace: "l" # cul-de-sac
#snakecase
Answers a snake cased string.
"ThisIsAnExample".snakecase # "this_is_an_example"
#squish
Removes leading, in body, and trailing whitespace, including any tabs or newlines, without mutating itself. Processes ASCII and unicode whitespace as well.
"one two three".squish # "one two three"
" one two \n \t three ".squish # "one two three"
#titleize
Answers a title string with proper capitalization of each word.
"ThisIsAnExample".titleize # "This Is An Example"
#truncate
Answers a truncated, non-mutated, string for given length with optional delimiter and/or overflow.
The delimiter is the second positional parameter (optional) and is nil
by default. A custom string or regular expression can be used to customize truncation behavior.
The trailer is an optional keyword parameter that is an ellipsis (i.e. "…"
) by default. The trailer can be a custom or empty string. The string length of the trailer is added to the length of the string being truncated, so keep this in mind when setting truncation length.
demo = "It was the best of times"
length = demo.length
demo.truncate 9 # "It was..."
demo.truncate 12 # "It was th..."
demo.truncate length # "It was the best of times"
demo.truncate Float::INFINITY # "It was the best of times"
demo.truncate 12, " " # "It was..."
demo.truncate 12, /\s/ # "It was..."
demo.truncate 6, trailer: "" # "It was"
demo.truncate 16, trailer: "... (more)" # "It was... (more)"
"demo".truncate 3 # "..."
#to_bool
Answers string as a boolean.
"true".to_bool # true
"yes".to_bool # true
"1".to_bool # true
"".to_bool # false
"example".to_bool # false
#up
Answers string with only first letter capitalized.
"example".up # "Example"
String IO
#reread
Answers full string by rewinding to beginning of string and reading all content.
io = StringIO.new
io.write "This is a test."
io.reread # "This is a test."
io.reread 4 # "This"
buffer = "".dup
io.reread(buffer:) # "This is a test."
buffer # "This is a test."
#to_s
Answers underlying string representation for explicit conversion.
io = StringIO.new
io.write "One"
io.write ", "
io.write "Two."
io.to_s # "One, Two."
#to_str
Answers underlying string representation for implicit conversion.
io = StringIO.new
io.write "One"
io.write ", "
io.write "Two."
io.to_str # "One, Two."
Struct
.with_positions
⚠️ This is deprecated and will be removed in Version 13.0.0.
Answers a struct instance with given positional arguments regardless of whether the struct was constructed with positional or keyword arguments.
Example = Struct.new :a, :b, :c
Example.with_positions 1, 2, 3 # #<struct a=1, b=2, c=3>
Example.with_positions 1 # #<struct a=1, b=nil, c=nil>
Example = Struct.new :a, :b, :c, keyword_init: true
Example.with_positions 1, 2, 3 # #<struct a=1, b=2, c=3>
Example.with_positions 1 # #<struct a=1, b=nil, c=nil>
#diff
Allows you to obtain the differences between two objects.
implementation = Struct.new :a, :b, :c
one = implementation.new a: 1, b: 2, c: 3
two = implementation.new a: 3, b: 2, c: 1
three = Struct.new(:x, :y).new x: 1, y: 2
one.diff one # {}
one.diff two # {:a=>[1, 3], :c=>[3, 1]}
one.diff three # {:a=>[1, nil], :b=>[2, nil], :c=>[3, nil]}
one.diff Object.new # {:a=>[1, nil], :b=>[2, nil], :c=>[3, nil]}
Any object that is not the same type will have a nil
value as shown in the last two examples.
#merge
Merges multiple attributes without mutating itself and supports any object that responds to #to_h
.
Works regardless of whether the struct is constructed with positional or keyword arguments.
example = Struct.new("Example", :a, :b, :c).new 1, 2, 3
other = Struct.new("Other", :a, :b, :c).new 7, 8, 9
example.merge a: 10 # #<struct Struct::Example a=10, b=2, c=3>
example.merge a: 10, c: 30 # #<struct Struct::Example a=10, b=2, c=30>
example.merge a: 10, b: 20, c: 30 # #<struct Struct::Example a=10, b=20, c=30>
example.merge other # #<struct Struct::Example a=7, b=8, c=9>
example # #<struct Struct::Example a=1, b=2, c=3>
#merge!
Merges multiple attributes while mutating itself and supports any object that responds to #to_h
.
Works regardless of whether the struct is constructed with positional or keyword arguments.
example = Struct.new("Example", :a, :b, :c).new 1, 2, 3
other = Struct.new("Other", :a, :b, :c).new 7, 8, 9
example.merge! a: 10 # #<struct Struct::Example a=10, b=2, c=3>
example.merge! a: 10, c: 30 # #<struct Struct::Example a=10, b=2, c=30>
example.merge! other # #<struct Struct::Example a=7, b=8, c=9>
example.merge! a: 10, b: 20, c: 30 # #<struct Struct::Example a=10, b=20, c=30>
example # #<struct Struct::Example a=10, b=20, c=30>
#revalue
Transforms values without mutating itself. An optional hash can be supplied to target specific attributes. In the event that a block isn’t supplied, the struct will answer itself since there is nothing to operate on. Works regardless of whether the struct is constructed with positional or keyword arguments.
example = Struct.new("Example", :a, :b, :c).new 1, 2, 3
example.revalue { |value| value * 2 } # #<struct Struct::Example a=2, b=4, c=6>
example.revalue(c: 2) { |previous, current| previous + current } # #<struct Struct::Example a=1, b=2, c=5>
example.revalue c: 2 # #<struct Struct::Example a=1, b=2, c=3>
example.revalue # #<struct Struct::Example a=1, b=2, c=3>
example # #<struct Struct::Example a=1, b=2, c=3>
#revalue!
Transforms values while mutating itself. An optional hash can be supplied to target specific attributes. In the event that a block isn’t supplied, the struct will answer itself since there is nothing to operate on. Works regardless of whether the struct is constructed with positional or keyword arguments.
one = Struct.new("One", :a, :b, :c).new 1, 2, 3
one.revalue! { |value| value * 2 } # #<struct Struct::One a=2, b=4, c=6>
one # #<struct Struct::One a=2, b=4, c=6>
two = Struct.new("Two", :a, :b, :c).new 1, 2, 3
two.revalue!(c: 2) { |previous, current| previous + current } # #<struct Struct::Two a=1, b=2, c=5>
two # #<struct Struct::Two a=1, b=2, c=5>
three = Struct.new("Three", :a, :b, :c).new 1, 2, 3
three.revalue! c: 2 # #<struct Struct::Three a=1, b=2, c=3>
three.revalue! # #<struct Struct::Three a=1, b=2, c=3>
three # #<struct Struct::Three a=1, b=2, c=3>
#transmute
Transmutes given enumerable by using the foreign key map and merging those key values into the current struct while not mutating itself. Works regardless of whether the struct is constructed with positional or keyword arguments.
a = Struct.new("A", :a, :b, :c).new 1, 2, 3
b = Struct.new("B", :x, :y, :z).new 7, 8, 9
c = {r: 10, s: 20, t: 30}
a.transmute b, a: :x, b: :y, c: :z # #<struct Struct::A a=7, b=8, c=9>
a.transmute b, b: :y # #<struct Struct::A a=1, b=8, c=3>
a.transmute c, c: :t # #<struct Struct::A a=1, b=2, c=30>
a # #<struct Struct::A a=1, b=2, c=3>
#transmute!
Transmutes given enumerable by using the foreign key map and merging those key values into the current struct while mutating itself. Works regardless of whether the struct is constructed with positional or keyword arguments.
a = Struct.new("A", :a, :b, :c).new 1, 2, 3
b = Struct.new("B", :x, :y, :z).new 7, 8, 9
c = {r: 10, s: 20, t: 30}
a.transmute! b, a: :x, b: :y, c: :z # #<struct Struct::A a=7, b=8, c=9>
a.transmute! b, b: :y # #<struct Struct::A a=1, b=8, c=3>
a.transmute! c, c: :t # #<struct Struct::A a=1, b=2, c=30>
a # #<struct Struct::A a=7, b=8, c=30>
#with
An alias of #merge
and identical in behavior (see #merge
documentation for details). Allows you to use Struct
and Data
objects more interchangeably since they share the same method.
Symbol
#call
Enhances symbol-to-proc functionality by allowing you to send positional, keyword, and/or a block arguments. This only works with public methods in order to not break encapsulation.
%w[clue crow cow].map(&:tr.call("c", "b")) # ["blue", "brow", "bow"]
[1.3, 1.5, 1.9].map(&:round.call(half: :up)) # [1, 2, 2]
[%w[a b c], %w[c a b]].map(&:index.call { |element| element == "b" }) # [1, 2]
%w[1.out 2.in].map(&:sub.call(/\./) { |bullet| bullet + " " }) # ["1. out", "2. in"]
[1, 2, 3].map(&:to_s.call) # ["1", "2", "3"]
⚠️ Use of #call
without any arguments should be avoided in order to not incur extra processing costs since the original symbol-to-proc call can be used instead.
Development
To contribute, run:
git clone https://github.com/bkuhlmann/refinements
cd refinements
bin/setup
You can also use the IRB console for direct access to all objects:
bin/console
Tests
To test, run:
bin/rake
Credits
-
Built with Gemsmith.
-
Engineered by Brooke Kuhlmann.