:toc: macro
:toclevels: 5
:figure-caption!:

= Refinements

[link=http://badge.fury.io/rb/refinements]
image::https://badge.fury.io/rb/refinements.svg[Gem Version]
[link=https://www.alchemists.io/projects/code_quality]
image::https://img.shields.io/badge/code_style-alchemists-brightgreen.svg[Alchemists Style Guide]
[link=https://circleci.com/gh/bkuhlmann/refinements]
image::https://circleci.com/gh/bkuhlmann/refinements.svg?style=svg[Circle CI Status]

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

toc::[]

== Features

Enhances the following objects:

* Array
* BigDecimal
* DateTime
* File
* Hash
* IO
* Pathname
* String
* StringIO

== Requirements

. https://www.ruby-lang.org[Ruby].
. A solid understanding of link:https://www.alchemists.io/articles/ruby_refinements[Ruby refinements
  and lexical scope].

== Setup

To install, run:

[source,bash]
----
gem install refinements
----

Add the following to your Gemfile file:

[source,ruby]
----
gem "refinements"
----

== Usage

=== Requires

If all refinements are not desired, add the following to your `+Gemfile+` instead:

[source,ruby]
----
gem "refinements", require: false
----

...then require the specific refinement, as needed. Example:

[source,ruby]
----
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):

[source,ruby]
----
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.

[source,ruby]
----
example = ["An", nil, "", "Example"]
example.compress  # => ["An", "Example"]
example           # => ["An", nil, "", "Example"]
----

===== #compress!

Removes `nil` and empty values while mutating itself.

[source,ruby]
----
example = ["An", nil, "", "Example"]
example.compress!  # => ["An", "Example"]
example            # => ["An", "Example"]
----

===== #exclude

Removes given array or elements without mutating itself.

[source,ruby]
----
[1, 2, 3, 4, 5].exclude [4, 5]  # => [1, 2, 3]
[1, 2, 3, 4, 5].exclude 4, 5    # => [1, 2, 3]
----

===== #include

Adds given array or elements without mutating itself.

[source,ruby]
----
[1, 2, 3].include [4, 5]  # => [1, 2, 3, 4, 5]
[1, 2, 3].include 4, 5    # => [1, 2, 3, 4, 5]
----

===== #intersperse

Inserts additional elements or array between all members of given array.

[source,ruby]
----
[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.

[source,ruby]
----
[].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.

[source,ruby]
----
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.

[source,ruby]
----
BigDecimal.new("5.0E-10").inspect # => "#<BigDecimal:3fd3d458fe84 0.0000000005>"
----

==== DateTime

===== .utc

Answers new DateTime object for current UTC date/time.

[source,ruby]
----
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.

[source,ruby]
----
File.rewrite("/test.txt") { |content| content.gsub "[placeholder]", "example" }
----

==== Hash

===== .infinite

Answers new hash where missing keys, even deeply nested, answer an empty hash.

[source,ruby]
----
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.

[source,ruby]
----
example = Hash.with_default ""
example[:a] # => ""

example = Hash.with_default []
example[:b] # => []
----

===== #deep_merge

Merges deeply nested hashes together without mutating itself.

[source,ruby]
----
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.

[source,ruby]
----
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.

[source,ruby]
----
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.

[source,ruby]
----
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.

[source,ruby]
----
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.

[source,ruby]
----
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.

[source,ruby]
----
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.

[source,ruby]
----
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.

[source,ruby]
----
{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.

[source,ruby]
----
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.

[source,ruby]
----
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.

[source,ruby]
----
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.

[source,ruby]
----
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.

[source,ruby]
----
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.

[source,ruby]
----
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.

[source,ruby]
----
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.

[source,ruby]
----
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.

[source,ruby]
----
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.

[source,ruby]
----
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.

[source,ruby]
----
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.

[source,ruby]
----
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.

[source,ruby]
----
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.

[source,ruby]
----
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.

[source,ruby]
----
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.

[source,ruby]
----
Pathname(nil) # => Pathname("")
----

===== #change_dir

Inherits and wraps `Dir.chdir` behavior by changing to directory of current path. See
link:https://rubyapi.org/2.7/o/s?q=Dir.chdir[Dir.chdir] for details.

[source,ruby]
----
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.

[source,ruby]
----
Pathname("input.txt").copy Pathname("output.txt")
----

===== #directories

Answers all or filtered directories for current path.

[source,ruby]
----
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.

[source,ruby]
----
Pathname("example.txt.erb").extensions # => [".txt", ".erb"]
----

===== #files

Answers all or filtered files for current path.

[source,ruby]
----
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.

[source,ruby]
----
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.

[source,ruby]
----
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.

[source,ruby]
----
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.

[source,ruby]
----
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.

[source,ruby]
----
Pathname("example.txt").name # => Pathname("example")
----

===== #relative_parent

Answers relative path from parent directory. This is a complement to `#relative_path_from`.

[source,ruby]
----
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.

[source,ruby]
----
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.

[source,ruby]
----
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.

[source,ruby]
----
Pathname("/test.txt").rewrite { |content| content.sub "[placeholder]", "example" }
----

===== #touch

Updates access and modification times for path. Defaults to current time.

[source,ruby]
----
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`.

[source,ruby]
----
" \n\t\r".blank? # => true
----

===== #camelcase

Answers a camelcased string.

[source,ruby]
----
"this_is_an_example".camelcase # => "ThisIsAnExample"
----

===== #down

Answers string with only first letter downcased.

[source,ruby]
----
"EXAMPLE".down # => "eXAMPLE"
----

===== #first

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

[source,ruby]
----
"example".first    # => "e"
"example".first 4  # => "exam"
----

===== #indent

Answers string indented by two spaces by default.

[source,ruby]
----
"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.

[source,ruby]
----
"instant".last    # => "t"
"instant".last 3  # => "ant"
----

===== #snakecase

Answers a snakecased string.

[source,ruby]
----
"ThisIsAnExample".snakecase # => "this_is_an_example"
----

===== #titleize

Answers titleized string.

[source,ruby]
----
"ThisIsAnExample".titleize # => "This Is An Example"
----

===== #to_bool

Answers string as a boolean.

[source,ruby]
----
"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.

[source,ruby]
----
"example".up # => "Example"
----

==== String IO

===== #reread

Answers full string by rewinding to beginning of string and reading all content.

[source,ruby]
----
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:

[source,bash]
----
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:

[source,bash]
----
bin/console
----

== Tests

To test, run:

[source,bash]
----
bundle exec rake
----

== Versioning

Read link:https://semver.org[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 link:CODE_OF_CONDUCT.adoc[CODE OF CONDUCT]. By
participating in this project you agree to abide by its terms.

== Contributions

Read link:CONTRIBUTING.adoc[CONTRIBUTING] for details.

== License

Read link:LICENSE.adoc[LICENSE] for details.

== History

Read link:CHANGES.adoc[CHANGES] for details.

== Credits

Engineered by link:https://www.alchemists.io/team/brooke_kuhlmann[Brooke Kuhlmann].