0.02
A long-lived project that still receives updates
A collection of refinements to core Ruby objects.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
 Project Readme

Refinements

Gem Version
Circle CI Status

A collection of refinements (enhancements) to primitive Ruby objects.

Table of Contents
  • Features
  • Requirements
  • Setup
  • Usage
    • Requires
    • Using
    • Examples
      • Array
        • #compress
        • #compress!
        • #include
        • #exclude
        • #ring
      • Big Decimal
        • #inspect
      • DateTime
        • .utc
      • File
        • .rewrite
      • Hash
        • .infinite
        • .with_default
        • #except
        • #except!
        • #flatten_keys
        • #flatten_keys!
        • #stringify_keys
        • #stringify_keys!
        • #symbolize_keys
        • #symbolize_keys!
        • #deep_merge
        • #deep_merge!
        • #deep_stringify_keys
        • #deep_stringify_keys!
        • #deep_symbolize_keys
        • #deep_symbolize_keys!
        • #recurse
        • #rekey
        • #rekey!
        • #reverse_merge
        • #reverse_merge!
        • #use
      • IO
        • .void
        • #squelch
        • #redirect
        • #reread
      • Pathname
        • Pathname
        • #name
        • #copy
        • #directories
        • #extensions
        • #files
        • #gsub
        • #relative_parent
        • #make_ancestors
        • #rewrite
        • #touch
      • String
        • #first
        • #last
        • #blank?
        • #up
        • #down
        • #indent
        • #camelcase
        • #snakecase
        • #titleize
        • #to_bool
      • String IO
        • #reread
  • 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

  1. Ruby.

  2. 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"]
#include

Adds given array or elements without mutating itself.

[1, 2, 3].include [4, 5]  # => [1, 2, 3, 4, 5]
[1, 2, 3].include 4, 5    # => [1, 2, 3, 4, 5]
#exclude

Removes given array or elements without mutating itself.

[1, 2, 3, 4, 5].exclude [4, 5]  # => [1, 2, 3]
[1, 2, 3, 4, 5].exclude 4, 5    # => [1, 2, 3]
#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] # => []
#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}
#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}
#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}}
#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}
#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
#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 # => ""
#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."

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("")
#name

Answers file name without extension.

Pathname("example.txt").name # => Pathname("example")
#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")
#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"
#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
#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

#first

Answers first character of a string or first set of characters if given a number.

"example".first    # => "e"
"example".first 4  # => "exam"
#last

Answers last character of a string or last set of characters if given a number.

"instant".last    # => "t"
"instant".last 3  # => "ant"
#blank?

Answers true/false based on whether string is blank, <space>, \n, \t, and/or \r.

" \n\t\r".blank? # => true
#up

Answers string with only first letter upcased.

"example".up # => "Example"
#down

Answers string with only first letter downcased.

"EXAMPLE".down # => "eXAMPLE"
#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"
#camelcase

Answers a camelcased string.

"this_is_an_example".camelcase # => "ThisIsAnExample"
#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

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.