: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 * Structs == 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" require "refinements/structs" ---- === 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 using Refinements::Structs 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"] ---- ===== #excluding Removes given array or elements without mutating itself. [source,ruby] ---- [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. [source,ruby] ---- [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. [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 # => # 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 # => "#" ---- ==== DateTime ===== .utc Answers new DateTime object for current UTC date/time. [source,ruby] ---- DateTime.utc # => # ---- ==== 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 = IO.void { |void| void.write "nevermore" } # => # ---- ===== #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.redirect(other) { |stream| stream.write "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) # => "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. [source,ruby] ---- io = IO.new IO.sysopen(Pathname("test.txt").to_s, "w+") io.squelch # => # io.squelch { io.write "Test" } # => # 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. [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 { "example" } # => "example" Pathname.pwd # => "/" ---- ===== #copy Copies file from current location to new location while answering itself so it can be chained. [source,ruby] ---- Pathname("input.txt").copy Pathname("output.txt") # => Pathname("input.txt") ---- ===== #directories Answers all directories 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 files 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/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. [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 # => Pathname("/test.txt") Pathname("/test.txt").rewrite { |body| body.sub "[token]", "example" } # => Pathname("/test.txt") ---- ===== #touch Updates access and modification times for path. Defaults to current time. [source,ruby] ---- Pathname("example.txt").touch # => Pathname("example.txt") Pathname("example.txt").touch at: 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. [source,ruby] ---- 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, ``, `\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) # => "This is a test." buffer # => "This is a test." ---- ==== Struct ===== #merge Merges multiple attributes without mutating itself. [source,ruby] ---- Example = Struct.new :a, :b, :c example = Example[1, 2, 3] example.merge a: 10 # => # example.merge a: 10, c: 30 # => # example.merge a: 10, b: 20, c: 30 # => # example # => # Example = Struct.new :a, :b, :c, keyword_init: true example = Example[a: 1, b: 2, c: 3] example.merge a: 10 # => # example.merge a: 10, c: 30 # => # example.merge a: 10, b: 20, c: 30 # => # example # => # ---- ===== #merge! Merges multiple attributes while mutating itself. [source,ruby] ---- Example = Struct.new :a, :b, :c example = Example[1, 2, 3] example.merge! a: 10 # => # example.merge! a: 10, c: 30 # => # example.merge! a: 10, b: 20, c: 30 # => # example # => # Example = Struct.new :a, :b, :c, keyword_init: true example = Example[a: 1, b: 2, c: 3] example.merge! a: 10 # => # example.merge! a: 10, c: 30 # => # example.merge! a: 10, b: 20, c: 30 # => # example # => # ---- == 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].