module NRSER
Namespace
¶ ↑
Namespace
¶ ↑
Namespace
¶ ↑
Namespace
¶ ↑
Namespace
¶ ↑
Namespace
¶ ↑
Functions for associating entries in an {Enumerable} as key or values in a {Hash}.
Definitions
¶ ↑
Definitions
¶ ↑
Definitions
¶ ↑
Definitions
¶ ↑
Namespace
¶ ↑
Namespace
¶ ↑
Definitions
¶ ↑
Functional methods to stylize a string through substitution
Namespace
¶ ↑
Namespace
¶ ↑
Namespace
¶ ↑
Namespace
¶ ↑
Namespace
¶ ↑
Namespace
¶ ↑
Namespace
¶ ↑
Namespace
¶ ↑
Declarations
¶ ↑
Namespace
¶ ↑
Namespace
¶ ↑
Namespace
¶ ↑
Declarations
¶ ↑
Declarations
¶ ↑
Namespace
¶ ↑
Namespace
¶ ↑
Namespace
¶ ↑
Namespace
¶ ↑
Namespace
¶ ↑
Namespace
¶ ↑
Namespace
¶ ↑
Namespace
¶ ↑
Namespace
¶ ↑
Namespace
¶ ↑
Namespace
¶ ↑
Namespace
¶ ↑
Namespace
¶ ↑
Namespace
¶ ↑
Namespace
¶ ↑
Namespace
¶ ↑
Namespace
¶ ↑
Namespace
¶ ↑
Namespace
¶ ↑
Namespace
¶ ↑
Namespace
¶ ↑
Namespace
¶ ↑
Namespace
¶ ↑
Namespace
¶ ↑
Namespace
¶ ↑
Namespace
¶ ↑
Namespace
¶ ↑
Namespace
¶ ↑
Namespace
¶ ↑
Namespace
¶ ↑
Namespace
¶ ↑
Constants
- FALSY_STRINGS
Down-cased versions of strings that are considered to communicate false in things like ENV vars, CLI options, etc.
@return [Set<String>]
- INDENT_RE
Constants
¶ ↑
- INDENT_TAG_MARKER
- INDENT_TAG_SEPARATOR
- JSON_ARRAY_RE
Regexp used to guess if a string is a JSON-encoded array.
@return [Regexp]
- JSON_OBJECT_RE
Regexp used to guess if a string is a JSON-encoded object.
@return [Regexp]
- NO_ARG
- ROOT
Absolute, expanded path to the gem's root directory.
Here in `//lib/nrser/version` so that it can be used via
require 'nrser/version'
without loading the entire module.
@return [Pathname]
- SPLIT_WORDS_RE
Regexp {NRSER.words} uses to split strings. Probably not great but it's what I have for the moment.
@return {Regexp}
- TRUTHY_STRINGS
Down-cased versions of strings that are considered to communicate true in things like ENV vars, CLI options, etc.
@return [Set<String>]
- UNICODE_ELLIPSIS
Unicode ellipsis character.
@return [String]
- VERSION
String
version of the gem.@return [String]
- WHITESPACE_RE
Regular expression used to match whitespace.
@return [Regexp]
Public Class Methods
# File lib/nrser/functions/open_struct.rb, line 22 def self._to_open_struct value, freeze: result = case value when OpenStruct # Just assume it's already taken care of if it's already an OpenStruct value when Hash OpenStruct.new( value.transform_values { |v| _to_open_struct v, freeze: freeze } ) when Array value.map { |v| _to_open_struct v, freeze: freeze } when Set Set.new value.map { |v| _to_open_struct v, freeze: freeze } else value end
Test slice inclusion when both the `slice` and the `enum` that we're going to look for it in support `#length` and `#slice` in the same manner that {Array} does (hence the name).
This is much simpler and more efficient than the “general” {Enumerable} case where we can't necessarily find out how many entries are in the enumerables or really do much of anything with them except iterate through (at least, with my current grasp of {Enumerable} and {Enumerator} it seems painfully complex… in fact it may never terminate for infinite enumerables).
@param [Enumerable<E> & length & slice] enum
The {Enumerable} that we want test for `slice` inclusion. Must support `#length` and `#slice` like {Array} does.
@param [Enumerable<S> & length & slice] slice
The {Enumerable} slice that we want to see if `enum` includes. Must support `#length` and `#slice` like {Array} does.
@param [Proc<(E, S)=>Boolean>] is_match
Optional {Proc} that accepts an entry from `enum` and an entry from `slice` and returns if they match.
@return [Boolean]
`true` if there is a slice of `enum` for which each entry matches the corresponding entry in `slice` according to `&is_match`.
# File lib/nrser/functions/enumerable/include_slice/array_include_slice.rb, line 33 def self.array_include_slice? enum, slice, &is_match slice_length = slice.length # Short-circuit on empty slice - it's *always* present return true if slice_length == 0 enum_length = enum.length # Short-circuit if slice is longer than enum since we can't possibly # match return false if slice_length > enum_length # Create a default `#==` matcher if we weren't provided one. if is_match.nil? is_match = ->(enum_entry, slice_entry) { enum_entry == slice_entry } end enum.each_with_index do |enum_entry, enum_start_index| # Compute index in `enum` that we would need to match up to enum_end_index = enum_start_index + slice_length - 1 # Short-circuit if can't match (more slice entries than enum ones left) return false if enum_end_index >= enum_length # Create the slice to test against enum_slice = enum[enum_start_index..enum_end_index] # See if every entry in the slice from `enum` matches the corresponding # one in `slice` return true if enum_slice.zip( slice ).all? { |(enum_entry, slice_entry)| is_match.call enum_entry, slice_entry } # Otherwise, just continue on through `enum` looking for that first # match until the number of `enum` entries left is too few for `slice` # to possibly match end # We never matched the first `slice` entry to a `enum` entry (and `slice` # had to be of length 1 so that the "too long" short-circuit never fired). # # So, we don't have a match. false end
Test if an object is “array-like” - is it an Enumerable
and does it respond to `#each_index`?
@param [Object] object
Any old thing.
@return [Boolean]
`true` if `object` is "array-like" for our purposes.
# File lib/nrser/functions/enumerable.rb, line 18 def self.array_like? object object.is_a?( ::Enumerable ) && object.respond_to?( :each_index ) end
Return an array given any value in the way that makes most sense:
-
If `value` is an array, return it.
-
If `value` is `nil`, return `[]`.
-
If `value` responds to `#to_a`, try calling it. If it succeeds, return that.
-
Return an array with `value` as it's only item.
Refinement
Added to `Object` in `nrser/refinements`.
@param [Object] value
@return [Array]
# File lib/nrser/functions/object/as_array.rb, line 25 def self.as_array value return value if value.is_a? Array return [] if value.nil? if value.respond_to? :to_a begin return value.to_a rescue end end [value] end
Treat the value as the value for `key` in a hash if it's not already a hash and can't be converted to one:
-
If the value is a `Hash`, return it.
-
If `value` is `nil`, return `{}`.
-
If the value responds to `#to_h` and `#to_h` succeeds, return the resulting hash.
-
Otherwise, return a new hash where `key` points to the value. **`key` MUST be provided in this case.**
Useful in method overloading and similar situations where you expect a hash that may specify a host of options, but want to allow the method to be called with a single value that corresponds to a default key in that option hash.
Refinement
Added to `Object` in `nrser/refinements`.
Example Time
!
Say you have a method `m` that handles a hash of HTML options that can look something like
{class: 'address', data: {confirm: 'Really?'}}
And can call `m` like
m({class: 'address', data: {confirm: 'Really?'}})
but often you are just dealing with the `:class` option. You can use {NRSER.as_hash} to accept a string and treat it as the `:class` key:
using NRSER def m opts opts = opts.as_hash :class # ... end
If you pass a hash, everything works normally, but if you pass a string `'address'` it will be converted to `{class: 'address'}`.
About `#to_h` Support
Right now, {.as_hash} also tests if `value` responds to `#to_h`, and will try to call it, using the result if it doesn't raise. This lets it deal with Ruby's “I used to be a Hash
until someone mapped me” values like `[[:class, 'address']]`. I'm not sure if this is the best approach, but I'm going to try it for now and see how it pans out in actual usage.
@todo
It might be nice to have a `check` option that ensures the resulting hash has a value for `key`.
@param [Object] value
The value that we want to be a hash.
@param [Object] key [default nil]
The key that `value` will be stored under in the result if `value` is not a hash or can't be turned into one via `#to_h`. If this happens this value can **NOT** be `nil` or an `ArgumentError` is raised.
@return [Hash]
@raise [ArgumentError]
If it comes to constructing a new Hash with `value` as a value and no argument was provided
# File lib/nrser/functions/object/as_hash.rb, line 82 def self.as_hash value, key = nil return value if value.is_a? Hash return {} if value.nil? if value.respond_to? :to_h begin return value.to_h rescue end end # at this point we need a key argument if key.nil? raise ArgumentError, "Need key to construct hash with value #{ value.inspect }, " + "found nil." end {key => value} end
Convert an enumerable to a hash by passing each entry through `&block` to get it's key, raising an error if multiple entries map to the same key.
@example Basic usage
['a', :b].assoc_by &:class # => {String=>"a", Symbol=>:b}
@example Conflict error
[:a, :b].assoc_by &:class # NRSER::ConflictError: Key Symbol is already in results with value: # # :a #
@param [Enumerable<V>] enum
Enumerable containing the values for the hash.
@param [Proc<(V)=>K>] block
Block that maps `enum` values to their hash keys.
@return [Hash<K, V>]
@raise [NRSER::ConflictError]
If two values map to the same key.
# File lib/nrser/functions/enumerable/associate.rb, line 35 def self.assoc_by enum, &block enum.each_with_object( {} ) { |element, result| key = block.call element if result.key? key raise NRSER::ConflictError.new binding.erb <<-END Key <%= key.inspect %> is already in results with value: <%= result[key].pretty_inspect %> END end result[key] = element } end
Create a {Hash} mapping the entries in `enum` to the value returned by passing them through `&block`, raising on conflicts.
@param [Enumerable<ENTRY>] enum
@param [ :raise | :first_wins | :last_wins | Proc ] on_conflict
What to do when there's a conflict mapping the entries into the hash. The names are meant to make some sense.
@param [Proc<(ENTRY)=>VALUE>] block
The star of the show! Maps `ENTRY` from `enum` to `VALUE` for the resulting hash.
@return [Hash<ENTRY, VALUE>]
@raise [NRSER::ConflictError]
If a conflict occurs and `on_conflict` is set to `:raise`.
# File lib/nrser/functions/enumerable/associate.rb, line 72 def self.assoc_to enum, on_conflict: :raise, &block enum.each_with_object( {} ) { |entry, hash| value = if hash.key? entry case on_conflict when :raise raise NRSER::ConflictError.new binding.erb <<-END Entry <%= entry %> appears more than once in `enum` This would cause conflict in the resulting {Hash}. Entry: <%= entry.pretty_inspect %> END when :first_wins # do nothing when :last_wins hash[entry] = block.call entry when Proc hash[entry] = on_conflict.call \ entry: entry, current_value: hash[entry], block: block else raise ArgumentError, "Bad `on_conflict`: #{ on_conflict.inspect }" end else block.call entry end hash[entry] = value } end
The opposite of `#dig` - set a value at a deep key path, creating necessary structures along the way and optionally clobbering whatever's in the way to achieve success.
@param [Hash] hash
Hash to bury the value in.
@param [Array | to_s] key_path
- When an {Array}, each entry is used exactly as-is for each key. - Otherwise, the `key_path` is converted to a string and split by `.` to produce the key array, and the actual keys used depend on the `parsed_key_type` option.
@param [Object] value
The value to set at the end of the path.
@param [Class | :guess] parsed_key_type
How to handle parsed key path segments: - `String` - use the strings that naturally split from a parsed key path. Note that this is the *String class itself, **not** a value that is a String*. - `Symbol` - convert the strings that are split from the key path to symbols. Note that this is the *Symbol class itself, **not** a value that is a Symbol*.`` - `:guess` (default) -
@return [return_type]
@todo Document return value.
# File lib/nrser/functions/hash/bury.rb, line 45 def self.bury! hash, key_path, value, parsed_key_type: :guess, clobber: false, create_arrays_for_unsigned_keys: false # Parse the key if it's not an array unless key_path.is_a?( Array ) key_path = key_path.to_s.split '.' # Convert the keys to symbols now if that's what we want to use if parsed_key_type == Symbol key_path.map! &:to_sym end end _internal_bury! \ hash, key_path, value, guess_key_type: ( parsed_key_type == :guess ), clobber: clobber, create_arrays_for_unsigned_keys: create_arrays_for_unsigned_keys end
Map *each entry* in `mappable` to a {NRSER::Message} and return a {Proc} that accepts a single `receiver` argument and reduces it by applying each message in turn.
In less precise terms: create a proc that chains the entries as methods calls.
@note
`mappable`` entries are mapped into messages when {#to_chain} is called, meaning subsequent changes to `mappable` **will not** affect the returned proc.
@example Equivalent of `Time.now.to_i`
NRSER::chainer( [:now, :to_i] ).call Time # => 1509628038
@return [Proc]
# File lib/nrser/functions/proc.rb, line 86 def self.chainer mappable, publicly: true messages = mappable.map { |value| message *value } ->( receiver ) { messages.reduce( receiver ) { |receiver, message| message.send_to receiver, publicly: publicly } } end
test if an object is considered a collection.
@param obj [Object] object to test @return [Boolean] true if `obj` is a collection.
# File lib/nrser/collection.rb, line 25 def collection? obj Collection::STDLIB.any? {|cls| obj.is_a? cls} || obj.is_a?(Collection) end
# File lib/nrser/functions/string.rb, line 44 def self.common_prefix strings raise ArgumentError.new("argument can't be empty") if strings.empty? sorted = strings.sort i = 0 while sorted.first[i] == sorted.last[i] && i < [sorted.first.length, sorted.last.length].min i = i + 1 end sorted.first[0...i] end
Count entries in an {Enumerable} by the value returned when they are passed to the block.
@example Count array entries by class
[1, 2, :three, 'four', 5, :six].count_by &:class # => {Fixnum=>3, Symbol=>2, String=>1}
@param [Enumerable<E>] enum
{Enumerable} (or other object with compatible `#each_with_object` and `#to_enum` methods) you want to count.
@param [Proc<(E)=>C>] block
Block mapping entries in `enum` to the group to count them in.
@return [Hash{C=>Integer}]
Hash mapping groups to positive integer counts.
# File lib/nrser/functions/enumerable.rb, line 203 def self.count_by enum, &block enum.each_with_object( Hash.new 0 ) do |entry, hash| hash[block.call entry] += 1 end end
# File lib/nrser/functions/text/indentation.rb, line 55 def self.dedent text, ignore_whitespace_lines: true, return_lines: false return text if text.empty? all_lines = if text.is_a?( Array ) text else text.lines end indent_significant_lines = if ignore_whitespace_lines all_lines.reject { |line| whitespace? line } else all_lines end indent = find_indent indent_significant_lines return text if indent.empty? dedented_lines = all_lines.map { |line| if line.start_with? indent line[indent.length..-1] elsif line.end_with? "\n" "\n" else "" end } if return_lines dedented_lines else dedented_lines.join end end
Get the directory for a path - if the path is a directory, it's returned (converted to a {Pathname}). It's not a directory, it's {Pathname#dirname} will be returned.
Expands the path (so that `~` paths work).
@param [String | Pathname] path
File or directory path.
@return [Pathname]
Absolute directory path.
# File lib/nrser/functions/path.rb, line 53 def self.dir_from path pn = pn_from( path ).expand_path if pn.directory? pn else pn.dirname end end
Yield on each element of a collection or on the object itself if it's not a collection. avoids having to normalize to an array to iterate over something that may be an object OR a collection of objects.
NOTE Implemented for our idea of a collection instead of testing
for response to `#each` (or similar) to avoid catching things like {IO} instances, which include {Enumerable} but are probably not what is desired when using {NRSER.each} (more likely that you mean "I expect one or more files" than "I expect one or more strings which may be represented by lines in an open {File}").
@param [Object] object
Target object.
@yield
Each element of a collection or the target object itself.
@return [Object]
`object` param.
# File lib/nrser/collection.rb, line 51 def each object, &block if collection? object # We need to test for response because {OpenStruct} *will* respond to # #each because *it will respond to anything* (which sucks), but it # will return `false` for `respond_to? :each` and the like, and this # behavior could be shared by other collection objects, so it seems # like a decent idea. if object.respond_to? :each_pair object.each_pair &block elsif object.respond_to? :each object.each &block else raise TypeError.squished <<-END Object #{ obj.inpsect } does not respond to #each or #each_pair END end else block.call object end object end
Enumerate over the immediate “branches” of a structure that can be used to compose our idea of a tree: nested hash-like and array-like structures like you would get from parsing a JSON document.
Written and tested against Hash
and Array
instances, but should work with anything hash-like that responds to `#each_pair` appropriately or array-like that responds to `#each_index` and `#each_with_index`.
@note Not sure what will happen if the tree has circular references!
@param [#each_pair | (each_index & each_with_index)] tree
Structure representing a tree via hash-like and array-like containers.
@yieldparam [Object] key
The first yielded param is the key or index for the value branch at the top level of `tree`.
@yieldparam [Object] value
The second yielded param is the branch at the key or index at the top level of `tree`.
@yieldreturn
Ignored.
@return [Enumerator]
If no block is provided.
@return [#each_pair | (each_index & each_with_index)]
If a block is provided, the result of the `#each_pair` or `#each_with_index` call.
@raise [NoMethodError]
If `tree` does not respond to `#each_pair` or to `#each_index` and `#each_with_index`.
# File lib/nrser/functions/tree/each_branch.rb, line 40 def self.each_branch tree, &block if tree.respond_to? :each_pair # Hash-like tree.each_pair &block elsif tree.respond_to? :each_index # Array-like... we test for `each_index` because - unintuitively - # `#each_with_index` is a method of {Enumerable}, meaning that {Set} # responds to it, though sets are unordered and the values can't be # accessed via those indexes. Hence we look for `#each_index`, which # {Set} does not respond to. if block.nil? index_enumerator = tree.each_with_index Enumerator.new( index_enumerator.size ) { |yielder| index_enumerator.each { |value, index| yielder.yield [index, value] } } else tree.each_with_index.map { |value, index| block.call [index, value] } end else raise NoMethodError.new NRSER.squish <<-END `tree` param must respond to `#each_pair` or `#each_index`, found #{ tree.inspect } END end # if / else end
Cut the middle out of a sliceable object with length and stick an ellipsis in there instead.
Categorized with {String} functions 'cause that's where it started, and that's probably how it will primarily continue to be used, but tested to work on {Array} and should for other classes that satisfy the same slice and interface.
@param [V & length & slice & << & +] source
Source object. In practice, {String} and {Array} work. In theory, anything that responds to `#length`, `#slice`, `#<<` and `#+` with the same semantics will work.
@param [Fixnum] max
Max length to allow for the output string.
@param [String] omission
The string to stick in the middle where original contents were removed. Defaults to the unicode ellipsis since I'm targeting the CLI at the moment and it saves precious characters.
@return [V]
Object of the same type as `source` of at most `max` length with the middle chopped out if needed to do so.\* \* Really, it has to do with how all the used methods are implemented, but we hope that conforming classes will return instances of their own class like {String} and {Array} do.
# File lib/nrser/functions/string.rb, line 128 def self.ellipsis source, max, omission: UNICODE_ELLIPSIS return source unless source.length > max trim_to = max - ( String === source ? omission.length : 1 ) middle = trim_to / 2 remainder = trim_to % 2 start = source.slice( 0, middle + remainder ) start << omission finish = source.slice( -( middle - remainder )..-1 ) start + finish end
Create an {Enumerator} that iterates over the “values” of an {Enumerable} `enum`. If `enum` responds to `#each_value` than we return that. Otherwise, we return `#each_entry`.
@param [Enumerable] enum
@return [Enumerator]
@raise [ArgumentError]
If `enum` doesn't respond to `#each_value` or `#each_entry`.
# File lib/nrser/functions/enumerable.rb, line 165 def self.enumerate_as_values enum # NRSER.match enum, # t.respond_to(:each_value), :each_value.to_proc, # t.respond_to(:each_entry), :each_entry.to_proc # if enum.respond_to? :each_value enum.each_value elsif enum.respond_to? :each_entry enum.each_entry else raise ArgumentError.new erb binding, <<-END Expected `enum` arg to respond to :each_value or :each_entry, found: <%= enum.inspect %> END end end
A destructive partition.
# File lib/nrser/functions/array.rb, line 17 def self.extract_from_array! array, &block extracted = [] array.reject! { |entry| test = block.call entry if test extracted << entry end test } extracted end
Opposite of {NRSER.truthy?}.
@pure Return value depends only on parameters.
@param object (see .truthy?)
@return [Boolean]
The negation of {NRSER.truthy?}.
@raise [ArgumentError]
When a string is received that is not in {NRSER::TRUTHY_STRINGS} or {NRSER::FALSY_STRINGS} (case insensitive).
@raise [TypeError]
When `object` is not the right type.
# File lib/nrser/functions/object/truthy.rb, line 107 def self.falsy? object ! truthy?(object) end
# File lib/nrser/functions/string.rb, line 60 def self.filter_repeated_blank_lines str, remove_leading: false out = [] lines = str.lines skipping = remove_leading str.lines.each do |line| if line =~ /^\s*$/ unless skipping out << line end skipping = true else skipping = false out << line end end out.join end
Find all truthy (not `nil` or `false`) results of calling `&block` with entries from `enum`.
@example
NRSER.find_all_map( [1, 2, 3, 4] ) do |i| if i.even? "#{ i } is even!" end end # => ["2 is even!", "4 is even!"]
@param [Enumerable<E>] enum
Entries to search (in order).
@param [Proc<(E)=>R>] block
Block mapping entires to results.
@return [nil]
When `block.call( E )` is `nil` or `false` for all `E` in `enum`.
@return [R]
The first result `R = block.call( E )` where `R` is not `nil` or `false`.
# File lib/nrser/functions/enumerable/find_all_map.rb, line 29 def self.find_all_map enum, &block enum.map( &block ).select { |entry| entry } end
Find all entries in an {Enumerable} for which `&block` returns a truthy value, then check the amount of results found against the {NRSER::Types.length} created from `bounds`, raising a {TypeError} if the results' length doesn't satisfy the bounds type.
@param [Enumerable<E>] enum
The entries to search and check.
@param [Integer | Hash] bounds
Passed as only argument to {NRSER::Types.length} to create the length type the results are checked against.
@param [Proc] block
`#find`/`#find_all`-style block that will be called with each entry from `enum`. Truthy responses mean the entry matched.
@return [Array<E>]
Found entries from `enum`.
@raise [TypeError]
If the results of `enum.find_all &block` don't satisfy `bounds`.
# File lib/nrser/functions/enumerable.rb, line 61 def self.find_bounded enum, bounds, &block NRSER::Types. length(bounds). check(enum.find_all &block) { |type:, value:| binding.erb <<-END Length of found elements (<%= value.length %>) FAILED to satisfy <%= type.to_s %>. Found entries: <%= value.pretty_inspect %> from enumerable: <%= enum.pretty_inspect %> END } end
Find the only entry in `enum` for which `&block` responds truthy, raising if either no entries or more than one are found.
Returns the entry itself, not an array of length 1.
Just calls {NRSER.find_bounded} with `bounds = 1`.
@param enum (see NRSER.find_bounded
) @param &block (see NRSER.find_bounded
)
@return [E]
Only entry in `enum` that `&block` matched.
@raise [TypeError]
If `&block` matched more or less than one entry.
# File lib/nrser/functions/enumerable.rb, line 99 def self.find_only enum, &block find_bounded(enum, 1, &block).first end
Ascend the directory tree starting at `from` (defaults to working directory) looking for a relative path.
How it works and what it returns is dependent on the sent options.
In the simplest / default case:
1.
@param [String | Pathname] rel_path
Relative path to search for. Can contains glob patterns; see the `glob` keyword.
@param [String | Pathname] from
Where to start the search. This is the first directory checked.
@param [Boolean | :guess] glob
Controls file-glob behavior with respect to `rel_path`: - `:guess` (default) - boolean value is computed by passing `rel_path` to {.looks_globish?}. - `true` - {Pathname.glob} is used to search for `rel_path` in each directory, and the first glob result that passes the test is considered the match. - `false` - `rel_path` is used as a literal file path (if it has a `*` character it will only match paths with a literal `*` character, etc.) **Be mindful that glob searches can easily consume significant resources when using broad patterns and/or large file trees.** Basically, you probably don't *ever* want to use `**` - we walk all the way up to the file system root, so it would be equivalent to searching *the entire filesystem*.
@todo
There should be a way to cut the search off early or detect `**` in the `rel_path` and error out or something to prevent full FS search.
@param [Symbol] test
The test to perform on pathnames to see if they match. Defaults to `:exist?` - which calls {Pathname#exist?} - but could be `:directory?` or anything else that makes sense.
@param [Symbol] result
What information to return: - `:common_root` (default) - return the directory that the match was relative to, so the return value is `from` or a ancestor of it. - `:path` - return the full path that was matched. - `:pair` - return the `:common_root` value followed by the `:path` value in a two-element {Array}.
@return [nil]
When no match is found.
@return [Pathname]
When a match is found and `result` keyword is - `:common_root` - the directory in `from.ascend` the match was made from. - `:path` - the path to the matched file.
@return [Array<(Pathname
, Pathname
)>]
When a match is found and `result` keyword is `:pair`, the directory the match was relative to followed by the matched path.
# File lib/nrser/functions/path.rb, line 141 def self.find_up( rel_path, from: Pathname.pwd, glob: :guess, test: :exist?, result: :common_root ) # If `glob` is `:guess`, override `glob` with the result of # {.looks_globish?} # glob = looks_globish?( rel_path ) if glob == :guess found = pn_from( from ).ascend.find_map { |dir| path = dir / rel_path found_path = if glob Pathname.glob( path ).find { |match_path| match_path.public_send test } elsif path.public_send( test ) path else nil end unless found_path.nil? [dir, found_path] end } return nil if found.nil? dir, path = found Types.match result, :common_root, dir, :pair, found, :path, path end
Exactly like {NRSER.find_up} but raises if nothing is found.
# File lib/nrser/functions/path.rb, line 184 def self.find_up! *args find_up( *args ).tap { |result| if result.nil? raise "HERE! #{ args.inspect }" end } end
Provides simple formatting for messages constructed as a list of segments.
Allows you to do this sort of thing:
NRSER.fmt_msg "Some stuff went wrong with the", thing, "and we're figuring it out, sorry. Maybe take a look at", something_else
Which I find easier than interpolation since you quite often have to split across lines anyways.
See {.fmt_msg_segment} for info about how each segment is formatted.
This methods joins the results together
@param [Array] segments
Message segments.
@return [String]
Formatted and joined message ready to pass up to the built-in exception's `#initialize`.
# File lib/nrser/functions/text/format.rb, line 49 def self.fmt_msg *segments segments.map { |segment| fmt_msg_segment segment }.join( ' ' ) end
Format a segment of a message.
If `segment` responds to `#to_summary`, it will be called and the result will be returned.
Strings are simply returned. Other things are inspected (for now).
@param [Object] segment
The segment.
@return [String]
The formatted string for the segment.
# File lib/nrser/functions/text/format.rb, line 16 def self.fmt_msg_segment segment return segment.to_summary.to_s if segment.respond_to?( :to_summary ) return segment if String === segment # TODO Do better! segment.inspect end
String
format an exception the same way they are printed to the CLI when not handled (when they crash programs - what you're used to seeing), including the message, class and backtrace.
@param [Exception] e
Exception to format.
@return [String]
# File lib/nrser/functions/exception.rb, line 13 def self.format_exception e "#{ e.to_s } (#{ e.class }):\n #{ e.backtrace.join("\n ") }" end
Get the absolute path to the root directory of the Git repo that `path` is in.
@note
In submodules, this will return the root of the submodule, **NOT** of the top-level repo.
@param [String | Pathname] path
Path in Git repo that you want to find the root of. Accepts relative and user (`~/...`) paths.
@return [Pathname]
# File lib/nrser/functions/git.rb, line 30 def self.git_root path = Pathname.getwd dir = dir_from path out, err, status = Open3.capture3 \ 'git rev-parse --show-toplevel', chdir: dir.to_s if status != 0 message = \ "#{ path.to_s.inspect } does not appear to be in a Git repo\n\n" + NRSER::Char::NULL.replace( err ) + "\n" raise SystemCallError.new message, status.exitstatus end Pathname.new out.chomp end
Guess which type of “label” key - strings or symbols - a hash (or other object that responds to `#keys` and `#empty`) uses.
@param [#keys & empty] keyed
Hash or similar object that responds to `#keys` and `#empty` to guess about.
@return [nil]
If we can't determine the type of "label" keys are used (there aren't any or there is a mix).
@return [Class]
If we can determine that {String} or {Symbol} keys are exclusively used returns that class.
# File lib/nrser/functions/hash/guess_label_key_type.rb, line 23 def self.guess_label_key_type keyed # We can't tell shit if the hash is empty return nil if keyed.empty? name_types = keyed. keys. map( &:class ). select { |klass| klass == String || klass == Symbol }. uniq return name_types[0] if name_types.length == 1 # There are both string and symbol keys present, we can't guess nil end
Test if an object is “hash-like” - is it an Enumerable
and does it respond to `#each_pair`?
@param [Object] object
Any old thing.
@return [Boolean]
`true` if `object` is "hash-like" for our purposes.
# File lib/nrser/functions/enumerable.rb, line 33 def self.hash_like? object object.is_a?( ::Enumerable ) && object.respond_to?( :each_pair ) end
See if an `enum` includes a `slice`, using an optional block to do custom matching.
@example Order matters
NRSER.slice? [1, 2, 3], [2, 3] # => true NRSER.slice? [1, 2, 3], [3, 2] # => false
@example The empty slice is always present
NRSER.slice? [1, 2, 3], [] # => true NRSER.slice? [], [] # => true
@example Custom `&is_match` block to prefix-match
NRSER.slice?( ['neil', 'mica', 'hudie'], ['m', 'h'] ) { |enum_entry, slice_entry| enum_entry.start_with? slice_entry } # => true
@note
Right now, just forwards to {NRSER.array_include_slice?}, which requires that the {Enumerable}s support {Array}-like `#length` and `#slice`. I took a swing at the general case but it came out messy and only partially correct.
@param [Enumerable] enum
Sequence to search in.
@param [Enumerable] slice
Slice to search for.
@return [Boolean]
`true` if `enum` has a slice matching `slice`.
# File lib/nrser/functions/enumerable/include_slice.rb, line 49 def self.include_slice? enum, slice, &is_match # Check that both args are {Enumerable} unless Enumerable === enum && Enumerable === slice raise TypeError.new binding.erb <<-END Both `enum` and `slice` must be {Enumerable} enum (<%= enum.class.safe_name %>): <%= enum.pretty_inspect %> slice (<%= slice.class.safe_name %>): <%= slice.pretty_inspect %> END end if [enum, slice].all? { |e| e.respond_to?( :length ) && e.respond_to?( :slice ) } return array_include_slice? enum, slice, &is_match end raise NotImplementedError.new binding.erb <<-END Sorry, but general {Enumerable} slice include has not been implemented It's kinda complicated, or at least seems that way at first, so I'm going to punt for now... END end
adapted from active_support 4.2.0
# File lib/nrser/functions/text/indentation.rb, line 31 def self.indent text, amount = 2, indent_string: nil, indent_empty_lines: false, skip_first_line: false if skip_first_line lines = self.lines text lines.first + indent( rest( lines ).join, amount, indent_string: indent_string, skip_first_line: false ) else indent_string = indent_string || text[/^[ \t]/] || ' ' re = indent_empty_lines ? /^/ : /^(?!$)/ text.gsub re, indent_string * amount end end
Tag each line of `text` with special marker characters around it's leading indent so that the resulting text string can be fed through an interpolation process like ERB that may inject multiline strings and the result can then be fed through {NRSER.indent_untag} to apply the correct indentation to the interpolated lines.
Each line of `text` is re-formatted like:
"<marker><leading_indent><separator><line_without_leading_indent>"
`marker` and `separator` can be configured via keyword arguments, but they
default to:
-
`marker` - {NRSER::INDENT_TAG_MARKER}, the no-printable ASCII *record separator* (ASCII character 30, “x1E” / “u001E”).
-
`separator` - {NRSER::INDENT_TAG_SEPARATOR}, the non-printable ASCII *unit separator* (ASCII character 31, “x1F” / “u001F”)
@example With default marker and separator
NRSER.indent_tag " hey there!" # => "\x1E \x1Fhey there!"
@param [String] text
String text to indent tag.
@param [String] marker
Special string to mark the start of tagged lines. If interpolated text lines start with this string you're going to have a bad time.
@param [String] separator
Special string to separate the leading indent from the rest of the line.
@return [String]
Tagged text.
# File lib/nrser/functions/text/indentation.rb, line 133 def self.indent_tag text, marker: INDENT_TAG_MARKER, separator: INDENT_TAG_SEPARATOR text.lines.map { |line| indent = if match = INDENT_RE.match( line ) match[0] else '' end "#{ marker }#{ indent }#{ separator }#{ line[indent.length..-1] }" }.join end
Reverse indent tagging that was done via {NRSER.indent_tag}, indenting any untagged lines to the same level as the one above them.
@param [String] text
Tagged text string.
@param [String] marker
Must be the marker used to tag the text.
@param [String] separator
Must be the separator used to tag the text.
@return [String]
Final text with interpolation and indent correction.
# File lib/nrser/functions/text/indentation.rb, line 163 def self.indent_untag text, marker: INDENT_TAG_MARKER, separator: INDENT_TAG_SEPARATOR current_indent = '' text.lines.map { |line| if line.start_with? marker current_indent, line = line[marker.length..-1].split( separator, 2 ) end current_indent + line }.join end
# File lib/nrser/functions/text/indentation.rb, line 22 def self.indented? text !( find_indent( text ).empty? ) end
# File lib/nrser/functions/string.rb, line 79 def self.lazy_filter_repeated_blank_lines source, remove_leading: false skipping = remove_leading source = source.each_line if source.is_a? String Enumerator::Lazy.new source do |yielder, line| if line =~ /^\s*$/ unless skipping yielder << line end skipping = true else skipping = false yielder << line end end end
Create a new hash where all the values are the scalar “leaves” of the possibly nested `hash` param. Leaves are keyed by “key path” arrays representing the sequence of keys to dig that leaf out of the has param.
In abstract, if `h` is the `hash` param and
l = NRSER.leaves h
then for each key `k` and corresponding value `v` in `l`
h.dig( *k ) == v
@pure
Return value depends only on parameters.
@example Simple “flat” hash
NRSER.leaves( {a: 1, b: 2} ) => { [:a] => 1, [:b] => 2, }
@example Nested hash
NRSER.leaves( 1 => { name: 'Neil', fav_color: 'blue', }, 2 => { name: 'Mica', fav_color: 'red', } ) # => { # [1, :name] => 'Neil', # [1, :fav_color] => 'blue', # [2, :name] => 'Mica', # [2, :fav_color] => 'red', # }
@param [#each_pair | (each_index & each_with_index)] tree
@return [Hash<Array, Object>]
# File lib/nrser/functions/tree/leaves.rb, line 51 def self.leaves tree {}.tap { |results| _internal_leaves tree, path: [], results: results } end
# File lib/nrser/functions/path.rb, line 64 def self.looks_globish? path %w|* ? [ {|.any? &path.to_s.method( :include? ) end
Test if a string looks like it might encode an array in JSON format by seeing if it's first non-whitespace character is `[` and last non-whitespace character is `]`.
@param [String] string
String to test.
@return [Boolean]
`true` if we think `string` encodes a JSON array.
# File lib/nrser/functions/string/looks_like.rb, line 46 def self.looks_like_json_array? string !!( string =~ JSON_ARRAY_RE ) end
Test if a string looks like it might encode an object in JSON format (JSON object becomes a {Hash} in Ruby) by seeing if it's first non-whitespace character is `{` and last non-whitespace character is `}`.
@param [String] string
String to test.
@return [Boolean]
`true` if we think `string` encodes a JSON object.
# File lib/nrser/functions/string/looks_like.rb, line 61 def self.looks_like_json_object? string !!( string =~ JSON_OBJECT_RE ) end
# File lib/nrser/functions/string/looks_like.rb, line 66 def self.looks_like_yaml_object? string # YAML is (now) a super-set of JSON, so anything that looks like a JSON # object is kosh looks_like_json_object?( string ) || string.lines.all? { |line| line.start_with?( '---', ' ', '#' ) || line =~ /[^\ ].*\:/ } end
If `object` is a collection, calls `#map` with the block. Otherwise, applies block to the object and returns the result.
See note in {NRSER.each} for discussion of why this tests for a collection instead of duck-typing `#map`.
@param [Object] object
Target object.
@yield
Each element of a collection or the target object itself.
@return [Object]
The result of mapping or applying the block.
# File lib/nrser/collection.rb, line 89 def map object, &block if collection? object object.map &block else block.call object end end
Map the immediate “branches” of a structure that can be used to compose our idea of a tree: nested hash-like and array-like structures like you would get from parsing a JSON document.
The `block` MUST return a pair ({Array} of length 2), the first value of which is the key or index in the new {Hash} or {Array}.
These pairs are then converted into a {Hash} or {Array} depending on it `tree` was {NRSER::Types.hash_like} or {NRSER::Types.array_like}, and that value is returned.
Uses {NRSER.each_branch} internally.
Written and tested against Hash
and Array
instances, but should work with anything:
-
hash-like that responds to `#each_pair` appropriately.
-
array-like that responds to `#each_index` and `#each_with_index` appropriately.
@pure
Return value depends only on parameters.
@note
Not sure what will happen if the tree has circular references!
@todo
Might be nice to have an option to preserve the tree class that creates a new instance of *whatever* it was and populates that, though I could see this relying on problematic assumptions and producing confusing results depending on the actual classes. Maybe this could be encoded in a mixin that we would detect or something.
@param [#each_pair | (each_index & each_with_index)] tree
Structure representing a tree via hash-like and array-like containers.
@yieldparam [Object] key
The first yielded param is the key or index for the value branch at the top level of `tree`.
@yieldparam [Object] value
The second yielded param is the branch at the key or index at the top level of `tree`.
@yieldreturn [Array]
Pair of key (/index) in new array or hash followed by value.
@return [Array | Hash]
If no block is provided.
@raise [TypeError | NoMethodError]
If `tree` does not respond to `#each_pair` or to `#each_index` and `#each_with_index`.
@raise [ArgumentError]
If `&block` is not provided.
# File lib/nrser/functions/tree/map_branches.rb, line 64 def self.map_branches tree, &block if block.nil? raise ArgumentError, "Must provide block" end pairs = each_branch( tree ).map &block if hash_like? tree pairs.to_h elsif array_like? tree pairs.each_with_object( [] ) { |(index, value), array| array[index] = value } else raise TypeError.new erb binding, <<-END Excepted `tree` arg to be array or hash-like. Received (<%= tree.class %>): <%= tree.pretty_inspect %> END end end
@!group Tree Functions
# File lib/nrser/functions/tree/map_leaves.rb, line 5 def self.map_leaves tree, &block NRSER::Types.tree.check tree _internal_map_leaves tree, key_path: [], &block end
Recursively descend through a tree mapping all non-structural elements
-
anything not {NRSER::Types.hash_like} or {NRSER::Types.array_like}, both
hash keys and values, as well as array entries - through `block` to produce a new structure.
Useful when you want to translate pieces of a tree structure depending on their type or some other property that can be determined *from the element alone* - `block` receives only the value as an argument, no location information (because it's weirder to represent for keys and I didn't need it for the {NRSER.transformer} stuff this was written for).
@note
Array indexes **are not mapped** through `block` and can not be changed via this method. This makes it easier to do things like "convert all the integers to strings" when you mean the data entries, not the array indexes (which would fail since the new array wouldn't accept string indices). If you don't want to map hash keys use {NRSER.map_leaves}.
See the specs for examples. Used in {NRSER.transformer}.
@param tree (see NRSER.each_branch
)
@param [Boolean] prune
When `true`, prunes out values whose labels end with `?` and values are `nil`.
@yieldparam [Object] element
Anything reached from the root that is not structural (hash-like or array-like), including / inside hash keys (though array indexes are **not** passed).
# File lib/nrser/functions/tree/map_tree.rb, line 35 def self.map_tree tree, prune: false, &block # TODO type check tree? mapped = tree.map { |element| # Recur if `element` is a tree. # # Since `element` will be an {Array} of `key`, `value` when `tree` is a # {Hash} (or similar), this will descend into hash keys that are also # trees, as well as into hash values and array entries. # if Types.tree.test element map_tree element, prune: prune, &block else # When we've run out of trees, finally pipe through the block: block.call element end } # If `tree` is hash-like, we want to convert the array of pair arrays # back into a hash. if Types.hash_like.test tree if prune pruned = {} mapped.each { |key, value| if Types.Label.test( key ) && key.to_s.end_with?( '?' ) unless value.nil? new_key = key.to_s[0..-2] if key.is_a?( Symbol ) new_key = new_key.to_sym end pruned[new_key] = value end else pruned[key] = value end } pruned else mapped.to_h end else # Getting here means it was array-like, so it's already fine mapped end end
Deep merge arrays of data hashes, matching hashes by computing a key with `&merge_key`.
Uses {NRSER.deep_merge!} to merge.
@param [Array<Hash>] current
Current (base) array of hashes to start with (lowest predominance).
@param [Array<Hash>] updates
One or more arrays of update hashes to merge over `current` (last is highest predominance).
@param [Proc<(Hash
)=>Object>] merge_key
Each hash is passed to `&merge_key` and the result is used to match hashes for merge. Must not return equal values for two different hashes in any of the arrays (`current` or any of `*updates`).
@return [Array<Hash>]
Final array of merged hashes. Don't depend on order.
# File lib/nrser/functions/merge_by.rb, line 23 def self.merge_by current, *updates, &merge_key updates.reduce( assoc_by current, &merge_key ) { |result, update| result.deep_merge! assoc_by( update, &merge_key ) }.values end
Creates a new {NRSER::Message} from the array.
@example
message = NRSER::Op.message( :fetch, :x ) message.send_to x: 'ex', y: 'why?' # => 'ex'
@return [NRSER::Message]
# File lib/nrser/functions/proc.rb, line 13 def self.message *args, &block if args.length == 1 && args[0].is_a?( Message ) args[0] else Message.new *args, &block end end
Core private method that supports all the other “method getters”.
@private
@param [Module] mod
Module in question.
@param [Boolean] include_super
When `true`, includes inherited class methods.
@param [:class | :instance] type
Get class or instance methods.
@param [Boolean] sort
If `true`, will sort the methods by name, which is usually the useful way to look at and use them.
@return [Array<(Method
| UnboundMethod
)>]
List of method objects (all bound to `mod`).
# File lib/nrser/functions/module/method_objects.rb, line 29 def self.method_objects_for mod, include_super, type:, sort:, include_initialize: false initialize_method = nil get_names, get_method = case type when :class [:methods, :method] when :instance if include_initialize # Only way I can figure out to find out if it is defined it to try # to get the object and handle the error begin initialize_method = mod.instance_method :initialize rescue NameError => error else # Don't want to include it if we're not `include_super` and it's # inherited from a different module unless include_super || initialize_method.owner == mod initialize_method = nil end end end [:instance_methods, :instance_method] else raise ArgumentError, "`type:` must be `:class` or `:instance`, found #{ type.inspect }" end # case type methods = mod.send( get_names, include_super ).map { |name| mod.send get_method, name } methods << initialize_method unless initialize_method.nil? methods.sort! { |a, b| a.name <=> b.name } if sort methods end
Test if a path is what I'm calling “normalized” - generally free of any `.`, `..` or empty segments, with specific exceptions for `'/'` and `'.'`.
@param [String | Pathname] path
Path to test.
@return [Boolean]
`true` if we consider the path "normalized".
@raise [NRSER::TypeError]
If `path` is not a {String} or {Pathname}.
# File lib/nrser/functions/path/normalized.rb, line 37 def self.normalized_path? path string = case path when String path when Pathname path.to_s else raise NRSER::TypeError.new \ "path must be String or Pathname, found", path, expected: [ String, Pathname ], found: path end # Examine each segment # NOTE The `-1` is *extremely* important - it stops suppression of empty # entries in the result, and we need them! segments = string.split File::SEPARATOR, -1 segments. # We need the indexes, since the first and last segments can be empty, # corresponding to `/...` and `.../` paths, respectively. each_with_index. # See if they all meet the requirements all? { |segment, index| ( segment != '.' || # Can't have any `.../x/./y/...` business index == 0 # But we can have `./x/y/` and such ) && segment != '..' && # Can't have any `.../x/../y/...` crap either ( # and, finally, the segment can't be empty segment != '' || # unless it's the first (`/x/...` case) index == 0 || # or the last segment (`.../z/` case) index == segments.length - 1 ) } end
Return the first entry if the enumerable has `#count` one.
Otherwise, return `default` (which defaults to `nil`).
@param [Enumerable<E>] enum
Enumerable in question (really, anything that responds to `#first` and `#count`).
@param [D] default
Value to return if `enum` does not have only one entry.
@return [E]
When `enum` has `#count == 1`.
@return [D]
When `enum` does not have `#count == 1`.
# File lib/nrser/functions/enumerable.rb, line 121 def self.only enum, default: nil if enum.count == 1 enum.first else default end end
Return the only entry if the enumerable has `#count` one. Otherwise raise an error.
@param enum (see NRSER.only
)
@return [E]
First element of `enum`.
@raise [ArgumentError]
If `enum` does not have `#count == 1`.
# File lib/nrser/functions/enumerable.rb, line 141 def self.only! enum count = enum.count unless count == 1 raise NRSER::CountError.new value: enum, count: count, expected: 1 end enum.first end
@return [Pathname]
# File lib/nrser/functions/path.rb, line 32 def self.pn_from path if path.is_a? Pathname path else Pathname.new path end end
Create a {Proc} that sends the arguments to a receiver via `#send`, forcing access to private and protected methods.
Equivalent to
message( symbol, *args, &block ).to_proc publicly: false
Pretty much here for completeness' sake.
@example
sender( :fetch, :x ).call x: 'ex' # => 'ex'
@return [Proc]
# File lib/nrser/functions/proc.rb, line 62 def self.private_sender symbol, *args, &block message( symbol, *args, &block ).to_proc publicly: false end
Create a {Proc} that sends the arguments to a receiver via `#public_send`.
Equivalent to
message( symbol, *args, &block ).to_proc
Pretty much here for completeness' sake.
@example
sender( :fetch, :x ).call x: 'ex' # => 'ex'
@return [Proc]
# File lib/nrser/functions/proc.rb, line 39 def self.public_sender symbol, *args, &block message( symbol, *args, &block ).to_proc end
Functional implementation of “rest” for arrays. Used when refining `#rest` into {Array}.
@param [Array] array
@return [return_type]
New array consisting of all elements after the first.
# File lib/nrser/functions/array.rb, line 11 def self.rest array array[1..-1] end
Return a {Proc} that accepts a single argument that must respond to `#[]` and retrieves `key` from it.
@param [String | Symbol
| Integer] key
Key (or index) to retrieve.
@return [Proc]
# File lib/nrser/functions/proc.rb, line 105 def self.retriever key ->( indexed ) { indexed[key] } end
Try to do “smart” job adding ellipsis to the middle of strings by splitting them by a separator `split` - that defaults to `, ` - then building the result up by bouncing back and forth between tokens at the beginning and end of the string until we reach the `max` length limit.
Intended to be used with possibly long single-line strings like `#inspect` returns for complex objects, where tokens are commonly separated by `, `, and producing a reasonably nice result that will fit in a reasonable amount of space, like `rspec` output (which was the motivation).
If `string` is already less than `max` then it is just returned.
If `string` doesn't contain `split` or just the first and last tokens alone would push the result over `max` then falls back to {NRSER.ellipsis}.
If `max` is too small it's going to fall back nearly always… around `64` has seemed like a decent place to start from screwing around on the REPL a bit.
@pure
Return value depends only on parameters.
@status
Experimental
@param [String] string
Source string.
@param [Fixnum] max
Max length to allow for the output string. Result will usually be *less* than this unless the fallback to {NRSER.ellipsis} kicks in.
@param [String] omission
The string to stick in the middle where original contents were removed. Defaults to the unicode ellipsis since I'm targeting the CLI at the moment and it saves precious characters.
@param [String] split
The string to tokenize the `string` parameter by. If you pass a {Regexp} here it might work, it might loop out, maybe.
@return [String]
String of at most `max` length with the middle chopped out if needed to do so.
# File lib/nrser/functions/string.rb, line 191 def self.smart_ellipsis string, max, omission: UNICODE_ELLIPSIS, split: ', ' return string unless string.length > max unless string.include? split return ellipsis string, max, omission: omission end tokens = string.split split char_budget = max - omission.length start = tokens[0] + split finish = tokens[tokens.length - 1] if start.length + finish.length > char_budget return ellipsis string, max, omission: omission end next_start_index = 1 next_finish_index = tokens.length - 2 next_index_is = :start next_index = next_start_index while ( start.length + finish.length + tokens[next_index].length + split.length ) <= char_budget do if next_index_is == :start start += tokens[next_index] + split next_start_index += 1 next_index = next_finish_index next_index_is = :finish else # == :finish finish = tokens[next_index] + split + finish next_finish_index -= 1 next_index = next_start_index next_index_is = :start end end start + omission + finish end # .smart_ellipsis # @!endgroup String Functions end
turn a multi-line string into a single line, collapsing whitespace to a single space.
same as ActiveSupport's String.squish, adapted from there.
# File lib/nrser/functions/string.rb, line 37 def self.squish str str.gsub(/[[:space:]]+/, ' ').strip end
Deeply convert a {Hash} to an {OpenStruct}.
@param [Hash] hash
@return [OpenStruct]
@raise [TypeError]
If `hash` is not a {Hash}.
# File lib/nrser/functions/open_struct.rb, line 12 def self.to_open_struct hash, freeze: false unless hash.is_a? Hash raise TypeError, "Argument must be hash (found #{ hash.inspect })" end _to_open_struct hash, freeze: freeze end
# File lib/nrser/functions/tree/transform.rb, line 4 def self.transform tree, source map_tree( tree, prune: true ) { |value| if value.is_a? Proc value.call source else value end } end
# File lib/nrser/functions/tree/transform.rb, line 35 def self.transformer &block map_tree( block.call SendSerializer.new ) { |value| if value.is_a? SendSerializer value.to_proc else value end } end
Evaluate an object (that probably came from outside Ruby, like an environment variable) to see if it's meant to represent true or false.
@pure Return value depends only on parameters.
@param [nil | String
| Boolean] object
Value to test.
@return [Boolean]
`true` if the object is "truthy".
@raise [ArgumentError]
When a string is received that is not in {NRSER::TRUTHY_STRINGS} or {NRSER::FALSY_STRINGS} (case insensitive).
@raise [TypeError]
When `object` is not the right type.
# File lib/nrser/functions/object/truthy.rb, line 64 def self.truthy? object case object when nil false when String downcased = object.downcase if TRUTHY_STRINGS.include? downcased true elsif FALSY_STRINGS.include? downcased false else raise ArgumentError, "String #{ object.inspect } not recognized as true or false." end when TrueClass, FalseClass object else raise TypeError, "Can't evaluate truthiness of #{ object.inspect }" end end
Like `Enumerable#find`, but wraps each call to `&block` in a `begin` / `rescue`, returning the result of the first call that doesn't raise an error.
If no calls succeed, raises a {NRSER::MultipleErrors} containing the errors from the block calls.
@param [Enumerable<E>] enum
Values to call `&block` with.
@param [Proc<E=>V>] block
Block to call, which is expected to raise an error if it fails.
@return [V]
Result of first call to `&block` that doesn't raise.
@raise [ArgumentError]
If `enum` was empty (`enum#each` never yielded).
@raise [NRSER::MultipleErrors]
If all calls to `&block` failed.
# File lib/nrser/functions/enumerable.rb, line 232 def self.try_find enum, &block errors = [] enum.each do |*args| begin result = block.call *args rescue Exception => error errors << error else return result end end if errors.empty? raise ArgumentError, "Appears that enumerable was empty: #{ enum.inspect }" else raise NRSER::MultipleErrors.new errors end end
Proxies to {NRSER::Char::AlphaNumericSub#sub} on {NRSER::Char::AlphaNumericSub.unicode_math_italic} to convert regular UTF-8/ASCII `a-zA-Z` characters to the “Unicode Math Italic” set.
@param [String] string
Input.
@return [String]
Output. Probably won't be `#ascii_only?`.
# File lib/nrser/functions/string/style.rb, line 37 def self.u_bold string NRSER::Char::AlphaNumericSub.unicode_math_bold.sub string end
Proxies to {NRSER::Char::AlphaNumericSub#sub} on {NRSER::Char::AlphaNumericSub.unicode_math_bold_italic} to convert regular UTF-8/ASCII `a-zA-Z` characters to the “Unicode Math Bold Italic” set.
@param [String] string
Input.
@return [String]
Output. Probably won't be `#ascii_only?`.
# File lib/nrser/functions/string/style.rb, line 52 def self.u_bold_italic string NRSER::Char::AlphaNumericSub.unicode_math_bold_italic.sub string end
Proxies to {NRSER::Char::AlphaNumericSub#sub} on {NRSER::Char::AlphaNumericSub.unicode_math_italic} to convert regular UTF-8/ASCII `a-zA-Z` characters to the “Unicode Math Italic” set.
@param [String] string
Input.
@return [String]
Output. Probably won't be `#ascii_only?`.
# File lib/nrser/functions/string/style.rb, line 22 def self.u_italic string NRSER::Char::AlphaNumericSub.unicode_math_italic.sub string end
Proxies to {NRSER::Char::AlphaNumericSub#sub} on {NRSER::Char::AlphaNumericSub.unicode_math_monospace} to convert regular UTF-8/ASCII `a-zA-Z` characters to the “Unicode Math Monospace” set.
@param [String] string
Input.
@return [String]
Output. Probably won't be `#ascii_only?`.
# File lib/nrser/functions/string/style.rb, line 67 def self.u_mono string NRSER::Char::AlphaNumericSub.unicode_math_monospace.sub string end
Indent tag a some text via {NRSER.indent_tag}, call the block with it, then pass the result through {NRSER.indent_untag} and return that.
@param [String] marker
Special string to mark the start of tagged lines. If interpolated text lines start with this string you're going to have a bad time.
@param [String] separator
Must be the separator used to tag the text.
@return [String]
Final text with interpolation and indent correction.
# File lib/nrser/functions/text/indentation.rb, line 195 def self.with_indent_tagged text, marker: INDENT_TAG_MARKER, separator: INDENT_TAG_SEPARATOR, &interpolate_block indent_untag( interpolate_block.call( indent_tag text, marker: marker, separator: separator ), marker: marker, separator: separator, ) end
Split text at whitespace to fit in line length. Lifted from Rails' ActionView.
@see api.rubyonrails.org/classes/ActionView/Helpers/TextHelper.html#method-i-word_wrap
@param [String] text
Text to word wrap.
@param [Fixnum] line_width
Line with in number of character to wrap at.
@param [String] break_sequence
String to join lines with.
@return [String]
@todo Document return value.
# File lib/nrser/functions/text/word_wrap.rb, line 21 def self.word_wrap text, line_width: 80, break_sequence: "\n" text.split("\n").collect! do |line| line.length > line_width ? line.gsub(/(.{1,#{line_width}})(\s+|$)/, "\\1#{break_sequence}").strip : line end * break_sequence end
Split a string into 'words' for word-based matching.
@param [String] string
Input string.
@return [Array<String>]
Array of non-empty words in `string`.
# File lib/nrser/functions/text/words.rb, line 19 def self.words string string.split(/[\W_\-\/]+/).reject { |w| w.empty? } end
Private Class Methods
# File lib/nrser/functions/hash/bury.rb, line 72 def self._internal_bury! tree, key_path, value, guess_key_type:, clobber:, create_arrays_for_unsigned_keys: # Split the key path into the current key and the rest of the keys key, *rest = key_path # If we are # # - Guessing the key type # - The tree is keyed # - The tree uses some {Symbol} (and no {String}) keys # # then convert the key to a symbol. # if guess_key_type && tree.respond_to?( :keys ) && guess_label_key_type( tree ) == Symbol key = key.to_sym end # Terminating case: we're at the last segment if rest.empty? # Set the value tree[key] = value else # Go deeper... # See if there is a hash in place unless NRSER::Types.tree.test tree[key] # There is not... so we need to do some figurin' # If we're clobbering or the hash has no value, we're good: # assign a new hash to set in if clobber || tree[key].nil? if create_arrays_for_unsigned_keys && NRSER::Types.unsigned.test( key ) tree[key] = [] else tree[key] = {} end else # We've got an intractable state conflict; raise raise NRSER::ConflictError.new squish <<-END can not set key #{ key.inspect } due to conflicting value #{ tree[key].inspect } in tree #{ tree.inspect } (:clobber option not set) END end end # unless hash[key].is_a?( Hash ) # Dive in... bury! tree[key], rest, value end # if rest.empty? / else end
Internal recursive implementation for {NRSER.leaves}.
@private
@pure
Return value depends only on parameters.
@param [#each_pair | (each_index & each_with_index)] tree
Tree to walk.
@param [Array] path
Key path down to `tree`.
@param [Hash<Array, Object>] results
New hash to stick results in.
@return [nil]
# File lib/nrser/functions/tree/leaves.rb, line 76 def self._internal_leaves tree, path:, results: NRSER.each_branch( tree ) { |key, value| new_path = [*path, key] if NRSER::Types.tree.test value _internal_leaves value, path: new_path, results: results else results[new_path] = value end } nil end
Internal recursive implementation for {NRSER.leaves}.
@param [#each_pair | (each_index & each_with_index)] tree
Tree to walk.
@param [Array] key_path
Key path down to `tree`.
@param [Proc] block
Called with each `(key_path, value)` pair.
@return [nil]
# File lib/nrser/functions/tree/map_leaves.rb, line 25 def self._internal_map_leaves tree, key_path:, &block NRSER::Types.match tree, NRSER::Types.hash_like, ->( hash_like ) { hash_like.map { |key, value| new_key_path = [*key_path, key] new_value = if NRSER::Types.tree.test( value ) _internal_map_leaves value, key_path: new_key_path, &block else block.call new_key_path, value end [key, new_value] }.to_h }, NRSER::Types.array_like, ->( array_like ) { array_like.each_with_index.map { |value, index| new_key_path = [*key_path, index] if NRSER::Types.tree.test( value ) _internal_map_leaves value, key_path: new_key_path, &block else block.call new_key_path, value end } } end