A collection of refinements (enhancements) to primitive Ruby objects.
- Features
- Requirements
- Setup
- Usage
- Requires
- Using
- Examples
- Array
- Big Decimal
- DateTime
- File
- Hash
- .infinite
- .with_default
- #deep_merge
- #deep_merge!
- #deep_stringify_keys
- #deep_stringify_keys!
- #deep_symbolize_keys
- #deep_symbolize_keys!
- #except
- #except!
- #flatten_keys
- #flatten_keys!
- #recurse
- #rekey
- #rekey!
- #reverse_merge
- #reverse_merge!
- #stringify_keys
- #stringify_keys!
- #symbolize_keys
- #symbolize_keys!
- #use
- IO
- Pathname
- String
- String IO
- Development
- Tests
- Versioning
- Code of Conduct
- Contributions
- License
- History
- Credits
Features
Enhances the following objects:
-
Array
-
BigDecimal
-
DateTime
-
File
-
Hash
-
IO
-
Pathname
-
String
-
StringIO
Requirements
-
Ruby.
-
A solid understanding of Ruby refinements and lexical scope.
Setup
To install, run:
gem install refinements
Add the following to your Gemfile file:
gem "refinements"
Usage
Requires
If all refinements are not desired, add the following to your Gemfile
instead:
gem "refinements", require: false
…then require the specific refinement, as needed. Example:
require "refinements/arrays"
require "refinements/big_decimals"
require "refinements/date_times"
require "refinements/files"
require "refinements/hashes"
require "refinements/ios"
require "refinements/pathnames"
require "refinements/strings"
require "refinements/string_ios"
Using
Much like including/extending a module, you’ll need to modify your object(s) to use the refinement(s):
class Example
using Refinements::Arrays
using Refinements::BigDecimals
using Refinements::DateTimes
using Refinements::Files
using Refinements::Hashes
using Refinements::IOs
using Refinements::Pathnames
using Refinements::Strings
using Refinements::StringIOs
end
Examples
The following sections demonstrate how each refinement enriches your objects with new capabilities.
Array
#compress
Removes nil
and empty values without mutating itself.
example = ["An", nil, "", "Example"]
example.compress # => ["An", "Example"]
example # => ["An", nil, "", "Example"]
#compress!
Removes nil
and empty values while mutating itself.
example = ["An", nil, "", "Example"]
example.compress! # => ["An", "Example"]
example # => ["An", "Example"]
#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]
#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 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]
#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
#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]
Big Decimal
#inspect
Allows one to inspect a big decimal with numeric representation.
BigDecimal.new("5.0E-10").inspect # => "#<BigDecimal:3fd3d458fe84 0.0000000005>"
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)>
File
.rewrite
When given a file path and a block, it provides the contents of the recently read file for manipulation and immediate writing back to the same file.
File.rewrite("/test.txt") { |content| content.gsub "[placeholder]", "example" }
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] # => []
#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
Stringifies keys of 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!
Stringifies 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}}
#except
Answers new hash with given keys removed without mutating itself.
example = {a: 1, b: 2, c: 3}
example.except :a, :b # => {c: 3}
example # => {a: 1, b: 2, c: 3}
#except!
Answers new hash with given keys removed while mutating itself.
example = {a: 1, b: 2, c: 3}
example.except! :a, :b # => {c: 3}
example # => {c: 3}
#flatten_keys
Flattens nested keys as top-level keys without mutating itself. Does not handle nested arrays, though.
{a: {b: 1}}.flatten_keys prefix: :test # => {test_a_b: 1}
{a: {b: 1}}.flatten_keys delimiter: :| # => {:"a|b" => 1}
{a: {b: 1}}.flatten_keys cast: :to_s # => {"a_b" => 1}
{"a" => {"b" => 1}}.flatten_keys cast: :to_sym # => {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. Does not handle nested arrays, though.
example = {a: {b: 1}}
example.flatten_keys! # => {a_b: 1}
example # => {a_b: 1}
#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"}
#rekey
Transforms keys per mapping (size of mapping can vary) without mutating itself.
example = {a: 1, b: 2, c: 3}
example.rekey a: :amber, b: :blue # => {amber: 1, blue: 2, c: 3}
example # => {a: 1, b: 2, c: 3}
#rekey!
Transforms keys per mapping (size of mapping can vary) while mutating itself.
example = {a: 1, b: 2, c: 3}
example.rekey! a: :amber, b: :blue # => {amber: 1, blue: 2, c: 3}
example # => {amber: 1, blue: 2, c: 3}
#reverse_merge
Merges calling hash into passed in hash without mutating itself.
example = {a: 1, b: 2}
example.reverse_merge a: 0, c: 3 # => {a: 1, b: 2, c: 3}
example # => {a: 1, b: 2}
#reverse_merge!
Merges calling hash into passed in hash while mutating itself.
example = {a: 1, b: 2}
example.reverse_merge! a: 0, c: 3 # => {a: 1, b: 2, c: 3}
example # => {a: 1, b: 2, c: 3}
#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}
#use
Passes each hash value as a block argument for further processing.
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.closed? # => false
io = IO.void { |void| void.write "nevermore" }
io.closed? # => true
#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("test.txt").to_s, "w+")
other = IO.new IO.sysopen(Pathname("other.txt").to_s, "w+")
io.redirect other # => `io`
io.redirect(other) { |stream| stream.write "test" }
.close # => ""
other.close # => "test"
#reread
Answers full stream by rewinding to beginning of stream and reading all content.
io = IO.new IO.sysopen(Pathname("test.txt").to_s, "w+")
io.write "This is a test."
io.reread # => "This is a test."
io.reread 4 # => "This"
buffer = "".dup
io.reread(buffer: buffer)
buffer # => "This is a test."
#squelch
Temporarily ignores any reads/writes for current stream for all code executed within the block. When not given a block, it answers itself.
io = IO.new IO.sysopen(Pathname("test.txt").to_s, "w+")
io.squelch { io.write "Test" }
io.reread # => ""
Pathname
Pathname
Enhances the conversion function — refined from Kernel
— which casts nil
into a pathname in
order to avoid: TypeError (no implicit conversion of nil into String)
. The pathname is still
invalid but at least you have an instance of Pathname
, which behaves like a Null Object, that
can still be used to construct a valid path.
Pathname(nil) # => Pathname("")
#change_dir
Inherits and wraps Dir.chdir
behavior by changing to directory of current path. See
Dir.chdir for details.
Pathname.pwd # => "/"
Pathname("/test").make_dir.change_dir # => Pathname "/test"
Pathname.pwd # => "/test"
Pathname.pwd # => "/"
Pathname("/test").make_dir.change_dir { # Implementation details } # => Pathname "/test"
Pathname.pwd # => "/"
#copy
Copies file from current location to new location.
Pathname("input.txt").copy Pathname("output.txt")
#directories
Answers all 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(".")]
#extensions
Answers file extensions as an array.
Pathname("example.txt.erb").extensions # => [".txt", ".erb"]
#files
Answers all 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").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")
#relative_parent
Answers relative path from parent directory. This is a complement to #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
child_path.remove_tree # => Pathname "/one/two"
child_path.exist? # => false
paremt_path.exist? # => true
child_path.make_path
parent_path.remove_tree # => Pathname "/one"
child_path.exist? # => false
parent_path.exist? # => false
#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 { |content| content.sub "[placeholder]", "example" }
#touch
Updates access and modification times for path. Defaults to current time.
Pathname("example.txt").touch
Pathname("example.txt").touch at: Time.now - 1
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 camelcased string.
"this_is_an_example".camelcase # => "ThisIsAnExample"
#down
Answers string with only first letter downcased.
"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 string indented by two spaces by default.
"example".indent # => " example"
"example".indent 0 # => "example"
"example".indent -1 # => "example"
"example".indent 2 # => " example"
"example".indent 3, padding: " " # => " example"
#last
Answers last character of a string or last set of characters if given a number.
"instant".last # => "t"
"instant".last 3 # => "ant"
#snakecase
Answers a snakecased string.
"ThisIsAnExample".snakecase # => "this_is_an_example"
#titleize
Answers titleized string.
"ThisIsAnExample".titleize # => "This Is An Example"
#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 upcased.
"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: buffer)
buffer # => "This is a test."
Development
To contribute, run:
git clone https://github.com/bkuhlmann/refinements.git
cd refinements
bin/setup
You can also use the IRB console for direct access to all objects:
bin/console
Tests
To test, run:
bundle exec rake
Versioning
Read Semantic Versioning for details. Briefly, it means:
-
Major (X.y.z) - Incremented for any backwards incompatible public API changes.
-
Minor (x.Y.z) - Incremented for new, backwards compatible, public API enhancements/fixes.
-
Patch (x.y.Z) - Incremented for small, backwards compatible, bug fixes.
Code of Conduct
Please note that this project is released with a CODE OF CONDUCT. By participating in this project you agree to abide by its terms.
Contributions
Read CONTRIBUTING for details.
License
Read LICENSE for details.
History
Read CHANGES for details.
Credits
Engineered by Brooke Kuhlmann.