Syndication Icon
Published July 19, 2015 Updated October 21, 2021
Refinements Icon

Refinements

Git Badge
Gem Version
Alchemists Style Guide
Circle CI Status

Refinements are a collection of enhancements to primitive Ruby objects without needing to resort to painful and hard to debug monkey patches. These refinements give you additional syntactic sugar to develop clean and concise implementations while using less code. By refining our code we can acquire the functionality we wish the core primitives had!

Features

Enhances the following objects:

  • Array

  • BigDecimal

  • DateTime

  • Hash

  • IO

  • Pathname

  • String

  • StringIO

  • Structs

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/classes"
require "refinements/date_times"
require "refinements/hashes"
require "refinements/ios"
require "refinements/pathnames"
require "refinements/strings"
require "refinements/string_ios"
require "refinements/structs"
require "refinements/symbols"

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::Classes
  using Refinements::DateTimes
  using Refinements::Hashes
  using Refinements::IOs
  using Refinements::Pathnames
  using Refinements::Strings
  using Refinements::StringIOs
  using Refinements::Structs
  using Refinements::Symbols
end

Examples

The following sections demonstrate how each refinement enriches your objects with new capabilities.

Array

#compress

Removes nil and empty objects without mutating itself.

object = Object.new
example = [1, "blueberry", nil, "", [], {}, object]

example.compress  # [1, "blueberry", object]
example           # [1, "blueberry", nil, "", [], {}, object]
#compress!

Removes nil and empty values while mutating itself.

object = Object.new
example = [1, "blueberry", nil, "", [], {}, object]

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

Answers the first truthy and filtered result from a 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 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]
#maximum

Answers the maximum extracted value from a collection of objects.

Point = Struct.new :x, :y, keyword_init: true
points = [Point[x: 1, y: 2], Point[x: 0, y: 1], Point[x: 2, y: 3]]

points.maximum(:x)  # 2
points.maximum(:y)  # 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
#minimum

Answers the minimum extracted value from a collection of objects.

Point = Struct.new :x, :y, keyword_init: true
points = [Point[x: 1, y: 2], Point[x: 0, y: 1], Point[x: 2, y: 3]]

points.minimum(:x)  # 0
points.minimum(:y)  # 1
#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.

[1].pad 0             # [1]
[1].pad 0, max: 3     # [1, 0, 0]
[1, 2].pad 3, max: 3  # [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>"

Class

#descendants

Answers descendants of a class.

a = Class.new
b = Class.new a
c = Class.new a

a.descendants          # [b, c]
Class.new.descendants  # []

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.

object = Object.new
example = {a: 1, b: "blueberry", c: nil, d: "", e: [], f: {}, g: object}

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.

object = Object.new
example = {a: 1, b: "blueberry", c: nil, d: "", e: [], f: {}, g: object}

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

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}}
#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: "test"}.fetch_value :a, "default"  # "test"
{a: "test"}.fetch_value :a             # "test"
{a: nil}.fetch_value :a, "default"     # "default"
{}.fetch_value(:a) { "default" }       # "default"
{}.fetch_value :a                      # KeyError
{a: "test"}.fetch_value                # ArgumentError
#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"}
#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: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("test.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 "test" }  # "#<IO:fd 21>"
#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)  # "This is a test."
buffer                     # "This is a test."
#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                       # ""

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 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/**/*.rb"

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 Bundler.root.join("spec"), "support/shared_examples/**/*.rb"
.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
#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")
#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                                           # 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 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"
#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", count: 0            # apples
"apple".pluralize "s", count: 1            # apple
"apple".pluralize "s", count: -1           # apple
"apple".pluralize "s", count: 2            # apples
"apple".pluralize "s", count: -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
"apples".singularize "s", count: 0            # apples
"apples".singularize "s", count: 1            # apple
"apples".singularize "s", count: -1           # apple
"apples".singularize "s", count: 2            # apples
"apples".singularize "s", count: -2           # apples
"cacti".singularize "i", replace: "us"        # cactus
"culs-de-sac".singularize "ls", replace: "l"  # cul-de-sac
#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)  # "This is a test."
buffer                     # "This is a test."

Struct

.keyworded?

Answers whether a struct was constructed with keyword or positional arguments.

Struct.new(:a, keyword_init: true).keyworded?  # true
Struct.new(:a).keyworded?                      # false
.with_keywords

Answers a struct instance with given keyword arguments regardless of whether the struct was constructed with positional or keyword arguments.

Example = Struct.new :a, :b, :c
Example.with_keywords a: 1, b: 2, c: 3  # "#<struct a=1, b=2, c=3>"
Example.with_keywords a: 1              # "#<struct a=1, b=nil, c=nil>"
Example.with_keywords c: 1              # "#<struct a=nil, b=nil, c=1>"

Example = Struct.new :a, :b, :c, keyword_init: true
Example.with_keywords a: 1, b: 2, c: 3  # "#<struct a=1, b=2, c=3>"
Example.with_keywords a: 1              # "#<struct a=1, b=nil, c=nil>"
Example.with_keywords c: 1              # "#<struct a=nil, b=nil, c=1>"
.with_positions

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>"
#merge

Merges multiple attributes without mutating itself.

Example = Struct.new :a, :b, :c
example = Example[1, 2, 3]
example.merge a: 10                # "#<struct a=10, b=2, c=3>"
example.merge a: 10, c: 30         # "#<struct a=10, b=2, c=30>"
example.merge a: 10, b: 20, c: 30  # "#<struct a=10, b=20, c=30>"
example                            # "#<struct a=1, b=2, c=3>"

Example = Struct.new :a, :b, :c, keyword_init: true
example = Example[a: 1, b: 2, c: 3]
example.merge a: 10                # "#<struct a=10, b=2, c=3>"
example.merge a: 10, c: 30         # "#<struct a=10, b=2, c=30>"
example.merge a: 10, b: 20, c: 30  # "#<struct a=10, b=20, c=30>"
example                            # "#<struct a=1, b=2, c=3>"
#merge!

Merges multiple attributes while mutating itself.

Example = Struct.new :a, :b, :c
example = Example[1, 2, 3]
example.merge! a: 10                # "#<struct a=10, b=2, c=3>"
example.merge! a: 10, c: 30         # "#<struct a=10, b=2, c=30>"
example.merge! a: 10, b: 20, c: 30  # "#<struct a=10, b=20, c=30>"
example                             # "#<struct a=10, b=20, c=30>"

Example = Struct.new :a, :b, :c, keyword_init: true
example = Example[a: 1, b: 2, c: 3]
example.merge! a: 10                # "#<struct a=10, b=2, c=3>"
example.merge! a: 10, c: 30         # "#<struct a=10, b=2, c=30>"
example.merge! a: 10, b: 20, c: 30  # "#<struct a=10, b=20, c=30>"
example                             # "#<struct a=10, b=20, c=30>"
#revalue

Transforms values without mutating itself. An optional hash can be supplied to pinpoint and transform specific attributes. In the event that a block isn’t supplied, the struct will answer itself since there is nothing to operate on. Behavior is the same regardless of whether the struct is constructed using positional or keyword arguments. A positional struct is used in the examples below but a keyword struct would work too.

Example = Struct.new :a, :b, :c

example = Example[1, 2, 3]
example.revalue { |value| value * 2 }                             # "#<struct a=2, b=4, c=6>"
example.revalue(c: 2) { |previous, current| previous + current }  # "#<struct a=1, b=2, c=5>"
example.revalue c: 2                                              # "#<struct a=1, b=2, c=3>"
example.revalue                                                   # "#<struct a=1, b=2, c=3>"
example                                                           # "#<struct a=1, b=2, c=3>"
#revalue!

Transforms values while mutating itself. An optional hash can be supplied to pinpoint and transform specific attributes. In the event that a block isn’t supplied, the struct will answer itself since there is nothing to operate on. Behavior is the same regardless of whether the struct is constructed using positional or keyword arguments. A positional struct is used in the examples below but a keyword struct would work too.

Example = Struct.new :a, :b, :c

example = Example[1, 2, 3]
example.revalue! { |value| value * 2 }                             # "#<struct a=2, b=4, c=6>"
example                                                            # "#<struct a=2, b=4, c=6>"

example = Example[1, 2, 3]
example.revalue!(c: 2) { |previous, current| previous + current }  # "#<struct a=1, b=2, c=5>"
example                                                            # "#<struct a=1, b=2, c=5>"

example = Example[1, 2, 3]
example.revalue! c: 2                                              # "#<struct a=1, b=2, c=3>"
example.revalue!                                                   # "#<struct a=1, b=2, c=3>"
example                                                            # "#<struct a=1, b=2, c=3>"

Symbol

#call

Enhances symbol-to-proc by allowing you to send additional arguments and/or a block. This only works with public methods in order to not break encapsulation.

%w[clue crow cow].map(&:tr.call("c", "b"))                              # ["blue", "brow", "bow"]
[%w[a b c], %w[c a b]].map(&:index.call { |element| element == "b" })   # [1, 2]
%w[1.outside 2.inside].map(&:sub.call(/\./) { |bullet| bullet + " " })  # ["1. outside", "2. inside"]
[1, 2, 3].map(&:to_s.call)                                              # ["1", "2", "3"]

⚠️ Use of #call without any arguments or block should be avoided in order to not incur extra processing costs since the original symbol-to-proc call can used instead.

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

Read Code of Conduct for details.

Contributions

Read Contributions for details.

License

Read License for details.

History

Read Changes for details.

Credits

Engineered by Brooke Kuhlmann.