trick_bag 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +19 -0
- data/Gemfile +4 -0
- data/Guardfile +8 -0
- data/LICENSE.txt +22 -0
- data/README.md +32 -0
- data/RELEASE_NOTES.md +3 -0
- data/Rakefile +1 -0
- data/lib/trick_bag.rb +7 -0
- data/lib/trick_bag/collections/linked_list.rb +97 -0
- data/lib/trick_bag/enumerables/buffered_enumerable.rb +90 -0
- data/lib/trick_bag/enumerables/compound_enumerable.rb +114 -0
- data/lib/trick_bag/enumerables/filtered_enumerable.rb +32 -0
- data/lib/trick_bag/io/temp_files.rb +22 -0
- data/lib/trick_bag/io/text_mode_status_updater.rb +59 -0
- data/lib/trick_bag/meta/classes.rb +87 -0
- data/lib/trick_bag/numeric/multi_counter.rb +44 -0
- data/lib/trick_bag/numeric/totals.rb +49 -0
- data/lib/trick_bag/operators/operators.rb +13 -0
- data/lib/trick_bag/timing/timing.rb +47 -0
- data/lib/trick_bag/validations/hash_validations.rb +21 -0
- data/lib/trick_bag/validations/object_validations.rb +26 -0
- data/lib/trick_bag/version.rb +3 -0
- data/spec/spec_helper.rb +11 -0
- data/spec/trick_bag/collections/linked_list_spec.rb +64 -0
- data/spec/trick_bag/enumerables/buffered_enumerable_spec.rb +117 -0
- data/spec/trick_bag/enumerables/compound_enumerable_spec.rb +141 -0
- data/spec/trick_bag/enumerables/filtered_enumerable_spec.rb +35 -0
- data/spec/trick_bag/io/temp_files_spec.rb +31 -0
- data/spec/trick_bag/io/text_mode_status_updater_spec.rb +24 -0
- data/spec/trick_bag/meta/classes_spec.rb +211 -0
- data/spec/trick_bag/numeric/multi_counter_spec.rb +28 -0
- data/spec/trick_bag/numeric/totals_spec.rb +38 -0
- data/spec/trick_bag/operators/operators_spec.rb +33 -0
- data/spec/trick_bag/timing/timing_spec.rb +17 -0
- data/spec/trick_bag/validations/hashes_validations_spec.rb +39 -0
- data/spec/trick_bag/validations/object_validations_spec.rb +23 -0
- data/trick_bag.gemspec +28 -0
- metadata +194 -0
@@ -0,0 +1,22 @@
|
|
1
|
+
module TrickBag
|
2
|
+
module Io
|
3
|
+
module TempFiles
|
4
|
+
|
5
|
+
module_function
|
6
|
+
|
7
|
+
def self.file_containing(text, file_prefix = '')
|
8
|
+
filespec = nil
|
9
|
+
begin
|
10
|
+
Tempfile.open(file_prefix) do |file|
|
11
|
+
file << text
|
12
|
+
filespec = file.path
|
13
|
+
end
|
14
|
+
yield(filespec)
|
15
|
+
ensure
|
16
|
+
File.delete filespec if filespec && File.exist?(filespec)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module TrickBag
|
2
|
+
module Io
|
3
|
+
|
4
|
+
# Updates the terminal line with text, erasing the original content and displaying at the same place.
|
5
|
+
# Uses ANSI escape sequences for cursor positioning and clearing
|
6
|
+
# (see http://www.oldlinux.org/Linux.old/Ref-docs/ASCII/ANSI%20Escape%20Sequences.htm).
|
7
|
+
class TextModeStatusUpdater
|
8
|
+
|
9
|
+
|
10
|
+
def initialize(text_generator, outstream = $stdout, force_output_non_tty = false)
|
11
|
+
@text_generator = text_generator
|
12
|
+
@outstream = outstream
|
13
|
+
@force_output_non_tty = force_output_non_tty
|
14
|
+
@first_time = true
|
15
|
+
end
|
16
|
+
|
17
|
+
def clear_to_end_of_line_text
|
18
|
+
"\x1b[2K"
|
19
|
+
end
|
20
|
+
|
21
|
+
def save_cursor_position_text
|
22
|
+
"\x1b[s"
|
23
|
+
end
|
24
|
+
|
25
|
+
def go_to_start_of_line_text
|
26
|
+
"\x1b0`"
|
27
|
+
end
|
28
|
+
|
29
|
+
def move_cursor_left_text(num_chars)
|
30
|
+
"\x1b[#{num_chars}D"
|
31
|
+
end
|
32
|
+
|
33
|
+
def restore_cursor_position_text
|
34
|
+
"\x1b[u"
|
35
|
+
end
|
36
|
+
|
37
|
+
def insert_blank_chars_text(num_chars)
|
38
|
+
"\x1b[#{num_chars}@"
|
39
|
+
end
|
40
|
+
|
41
|
+
def print
|
42
|
+
|
43
|
+
# If output is being redirected, don't print anything; it will look like garbage;
|
44
|
+
# But if output was forced (e.g. to write to a string), then allow it.
|
45
|
+
return unless @outstream.tty? || @force_output_non_tty
|
46
|
+
|
47
|
+
if @first_time
|
48
|
+
@first_time = false
|
49
|
+
else
|
50
|
+
@outstream.print(move_cursor_left_text(@prev_text_length))
|
51
|
+
end
|
52
|
+
text = @text_generator.()
|
53
|
+
@prev_text_length = text.length
|
54
|
+
@outstream.print(clear_to_end_of_line_text + text)
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
# Provides useful additions to class Class.
|
2
|
+
|
3
|
+
module TrickBag
|
4
|
+
module Meta
|
5
|
+
module Classes
|
6
|
+
|
7
|
+
VALID_ACCESS_MODES = [:public, :protected, :private, :none]
|
8
|
+
|
9
|
+
# Enables concise and flexible creation of getters and setters
|
10
|
+
# whose access modes for reading and writing may be different.
|
11
|
+
#
|
12
|
+
# Access modes can be: :public, :protected, :private, :none
|
13
|
+
#
|
14
|
+
# For example:
|
15
|
+
#
|
16
|
+
# attr_access(:private, :none, :foo, :bar)
|
17
|
+
#
|
18
|
+
# will create accessors for @foo and @bar, and make them private,
|
19
|
+
# but will not create any mutators.
|
20
|
+
def attr_access(read_access, write_access, *attrs)
|
21
|
+
validate_input = -> do
|
22
|
+
[read_access, write_access].each do |access|
|
23
|
+
unless VALID_ACCESS_MODES.include?(access)
|
24
|
+
raise "Access mode must be one of [:public, :protected, :private]."
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
validate_input.()
|
30
|
+
|
31
|
+
unless read_access == :none
|
32
|
+
attr_reader(*attrs)
|
33
|
+
send(read_access, *attrs)
|
34
|
+
end
|
35
|
+
|
36
|
+
unless write_access == :none
|
37
|
+
attr_writer(*attrs)
|
38
|
+
writers = attrs.map { |attr| "#{attr}=".to_sym }
|
39
|
+
send(write_access, *writers)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
def private_attr_reader(*attrs)
|
45
|
+
attr_access(:private, :none, *attrs)
|
46
|
+
end
|
47
|
+
|
48
|
+
|
49
|
+
def private_attr_writer(*attrs)
|
50
|
+
attr_access(:none, :private, *attrs)
|
51
|
+
end
|
52
|
+
|
53
|
+
|
54
|
+
def private_attr_accessor(*attrs)
|
55
|
+
attr_access(:private, :private, *attrs)
|
56
|
+
end
|
57
|
+
|
58
|
+
|
59
|
+
def protected_attr_reader(*attrs)
|
60
|
+
attr_access(:protected, :none, *attrs)
|
61
|
+
end
|
62
|
+
|
63
|
+
|
64
|
+
def protected_attr_writer(*attrs)
|
65
|
+
attr_access(:none, :protected, *attrs)
|
66
|
+
end
|
67
|
+
|
68
|
+
|
69
|
+
def protected_attr_accessor(*attrs)
|
70
|
+
attr_access(:protected, :protected, *attrs)
|
71
|
+
end
|
72
|
+
|
73
|
+
|
74
|
+
def class?(name, scope = Object)
|
75
|
+
scope.const_defined?(name) && scope.const_get(name).is_a?(Class)
|
76
|
+
end; module_function :class?
|
77
|
+
|
78
|
+
|
79
|
+
def undef_class(name, scope = Object)
|
80
|
+
class_existed = class?(name, scope)
|
81
|
+
scope.send(:remove_const, name) if class_existed
|
82
|
+
class_existed
|
83
|
+
end; module_function :undef_class
|
84
|
+
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module TrickBag
|
2
|
+
module Numeric
|
3
|
+
|
4
|
+
# Counts the results of any resolver comparisons made.
|
5
|
+
# Like a hash, but does not allow []=; increment is the only way to modify a value.
|
6
|
+
class MultiCounter
|
7
|
+
|
8
|
+
attr_accessor :name
|
9
|
+
|
10
|
+
def initialize(name = '')
|
11
|
+
@name = name
|
12
|
+
@counts = Hash.new(0)
|
13
|
+
end
|
14
|
+
|
15
|
+
def add_keys(keys)
|
16
|
+
keys.each { |key| @counts[key] = 0 }
|
17
|
+
end
|
18
|
+
|
19
|
+
def increment(key)
|
20
|
+
@counts[key] += 1
|
21
|
+
end
|
22
|
+
|
23
|
+
def [](key)
|
24
|
+
@counts[key]
|
25
|
+
end
|
26
|
+
|
27
|
+
def keys
|
28
|
+
@counts.keys
|
29
|
+
end
|
30
|
+
|
31
|
+
def key_exists?(key)
|
32
|
+
keys.include?(key)
|
33
|
+
end
|
34
|
+
|
35
|
+
def to_hash
|
36
|
+
@counts.clone
|
37
|
+
end
|
38
|
+
|
39
|
+
def to_s
|
40
|
+
"#{self.class} '#{name}': #{@counts.to_s}"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module TrickBag
|
2
|
+
module Numeric
|
3
|
+
module Totals
|
4
|
+
|
5
|
+
module_function
|
6
|
+
|
7
|
+
# @inputs an enumerable of numbers
|
8
|
+
# @return a collection containing the corresponding fractions of total of those numbers
|
9
|
+
def map_fraction_of_total(inputs)
|
10
|
+
return [] if inputs.size == 0
|
11
|
+
sum = Float(inputs.inject(:+))
|
12
|
+
inputs.map { |n| n / sum }
|
13
|
+
end
|
14
|
+
|
15
|
+
|
16
|
+
# @inputs an enumerable of numbers
|
17
|
+
# @return a collection containing the corresponding percents of total of those numbers
|
18
|
+
def map_percent_of_total(inputs)
|
19
|
+
map_fraction_of_total(inputs).map { |n| n * 100 }
|
20
|
+
end
|
21
|
+
|
22
|
+
|
23
|
+
# Given a hash whose values are numbers, produces a new hash with the same keys
|
24
|
+
# as the original hash, but whose values are the % of total.
|
25
|
+
# Ex: for input { foo: 100, bar: 200, baz: 300, razz: 400 }, value returned would be
|
26
|
+
# { foo: 10.0, bar: 20.0, baz: 30.0, razz: 40.0 }.
|
27
|
+
def fraction_of_total_hash(the_hash)
|
28
|
+
new_hash = percent_of_total_hash(the_hash)
|
29
|
+
new_hash.keys.each do |key|
|
30
|
+
new_hash[key] = new_hash[key] / 100.0
|
31
|
+
end
|
32
|
+
new_hash
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
# Given a hash whose values are numbers, produces a new hash with the same keys
|
37
|
+
# as the original hash, but whose values are the % of total.
|
38
|
+
# Ex: for input { foo: 100, bar: 200, baz: 300, razz: 400 }, value returned would be
|
39
|
+
# { foo: 10.0, bar: 20.0, baz: 30.0, razz: 40.0 }.
|
40
|
+
def percent_of_total_hash(the_hash)
|
41
|
+
sum = Float(the_hash.values.inject(:+))
|
42
|
+
keys = the_hash.keys
|
43
|
+
keys.each_with_object({}) do |key, percent_total_hash|
|
44
|
+
percent_total_hash[key] = 100 * the_hash[key] / sum
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module TrickBag
|
2
|
+
module Operators
|
3
|
+
|
4
|
+
module_function
|
5
|
+
|
6
|
+
# Returns whether or not all passed values are equal
|
7
|
+
def multi_eq(*values)
|
8
|
+
values = values.first if values.is_a?(Array) && values.size == 1
|
9
|
+
raise "Must be called with at least 2 parameters" if values.size < 2
|
10
|
+
values[1..-1].all? { |value| value == values.first }
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'trick_bag/io/text_mode_status_updater'
|
2
|
+
|
3
|
+
module TrickBag
|
4
|
+
module Timing
|
5
|
+
|
6
|
+
module_function
|
7
|
+
|
8
|
+
|
9
|
+
# Calls a predicate proc repeatedly, sleeping the specified interval
|
10
|
+
# between calls, and giving up after the specified number of seconds.
|
11
|
+
# Displays elapsed and remaining times on the terminal.
|
12
|
+
#
|
13
|
+
# @param predicate something that can be called with .() or .call
|
14
|
+
# that returns a truthy value that indicates no further retries are necessary
|
15
|
+
# @param sleep_interval number of seconds (fractions ok) to wait between tries
|
16
|
+
# @param timeout_secs maximum number of seconds (fractions ok) during which to retry
|
17
|
+
def retry_until_true_or_timeout(
|
18
|
+
predicate, sleep_interval, timeout_secs, output_stream = $stdout)
|
19
|
+
|
20
|
+
success = false
|
21
|
+
start_time = Time.now
|
22
|
+
end_time = start_time + timeout_secs
|
23
|
+
time_elapsed = nil
|
24
|
+
time_to_go = nil
|
25
|
+
text_generator = ->() { "%9.3f %9.3f" % [time_elapsed, time_to_go] }
|
26
|
+
status_updater = ::TrickBag::Io::TextModeStatusUpdater.new(text_generator, output_stream)
|
27
|
+
|
28
|
+
loop do
|
29
|
+
now = Time.now
|
30
|
+
time_elapsed = now - start_time
|
31
|
+
time_to_go = end_time - now
|
32
|
+
time_up = now >= end_time
|
33
|
+
|
34
|
+
break if time_up
|
35
|
+
|
36
|
+
success = !! predicate.()
|
37
|
+
break if success
|
38
|
+
|
39
|
+
status_updater.print
|
40
|
+
sleep(sleep_interval)
|
41
|
+
end
|
42
|
+
print "\n"
|
43
|
+
success
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module TrickBag
|
2
|
+
module Validations
|
3
|
+
module HashValidations
|
4
|
+
module_function
|
5
|
+
|
6
|
+
# Looks to see which keys, if any, are missing from the hash.
|
7
|
+
# @return an array of missing keys (empty if none)
|
8
|
+
def missing_hash_entries(hash, keys)
|
9
|
+
keys.reject { |key| hash.keys.include?(key) }
|
10
|
+
end
|
11
|
+
|
12
|
+
# Looks to see which keys, if any, are missing from the hash.
|
13
|
+
# @return nil if none missing, else comma separated string of missing keys.
|
14
|
+
def missing_hash_entries_as_string(hash, keys)
|
15
|
+
missing_keys = missing_hash_entries(hash, keys)
|
16
|
+
missing_keys.empty? ? nil : missing_keys.join(', ')
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module TrickBag
|
2
|
+
module Validations
|
3
|
+
module ObjectValidations
|
4
|
+
|
5
|
+
module_function
|
6
|
+
|
7
|
+
|
8
|
+
# Returns an array containing each symbol in vars for which the
|
9
|
+
# corresponding instance variable in the specified object is nil.
|
10
|
+
def nil_instance_vars(object, vars)
|
11
|
+
vars = Array(vars)
|
12
|
+
vars.select { |var| object.instance_variable_get(var).nil? }
|
13
|
+
end
|
14
|
+
|
15
|
+
# For each symbol in vars, checks to see that the corresponding instance
|
16
|
+
# variable in the specified object is not nil.
|
17
|
+
# If any are nil, raises an error listing the nils.
|
18
|
+
def raise_on_nil_instance_vars(object, vars)
|
19
|
+
nil_vars = nil_instance_vars(object, vars)
|
20
|
+
unless nil_vars.empty?
|
21
|
+
raise "The following instance variables were nil: #{nil_vars.join(', ')}."
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'rspec'
|
2
|
+
require 'pry'
|
3
|
+
|
4
|
+
$LOAD_PATH << File.join(File.dirname(__FILE__), '..', 'lib')
|
5
|
+
|
6
|
+
RSpec.configure do |config|
|
7
|
+
config.expect_with :rspec do |c|
|
8
|
+
# disable the `should` syntax; it's deprecated and will later be removed
|
9
|
+
c.syntax = :expect
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# Adapted from Test Unit test at https://github.com/neilparikh/ruby-linked-list.
|
2
|
+
|
3
|
+
require_relative '../../spec_helper'
|
4
|
+
require 'trick_bag/collections/linked_list'
|
5
|
+
|
6
|
+
module TrickBag
|
7
|
+
module Collections
|
8
|
+
|
9
|
+
describe LinkedList do
|
10
|
+
|
11
|
+
specify 'to_a should return an array equal to the array with which it was initialized' do
|
12
|
+
array = [1, 3, 5]
|
13
|
+
expect(LinkedList.new(*array).to_a).to eq(array)
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'should push correctly' do
|
17
|
+
list = LinkedList.new(1)
|
18
|
+
list.push(2)
|
19
|
+
expect(list.to_a).to eq([1, 2])
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'should pop from a list of multiple elements correctly' do
|
23
|
+
list = LinkedList.new(1, 2, 3)
|
24
|
+
expect(list.pop).to eq(3)
|
25
|
+
expect(list.to_a).to eq([1, 2])
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'should pop from a list of 1 element correctly' do
|
29
|
+
list = LinkedList.new(1)
|
30
|
+
expect(list.pop).to eq(1)
|
31
|
+
expect(list.to_a).to eq([])
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'should raise an error when popping from an empty list' do
|
35
|
+
list = LinkedList.new
|
36
|
+
expect(list.length).to eq(0)
|
37
|
+
expect(->{ list.pop }).to raise_error(RuntimeError)
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'should unshift correctly' do
|
41
|
+
list = LinkedList.new(1)
|
42
|
+
list.unshift(0)
|
43
|
+
expect(list.to_a).to eq([0,1])
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'should shift correctly from a list containing multiple elements' do
|
47
|
+
list = LinkedList.new(1, 3, 4)
|
48
|
+
expect(list.shift).to eq(1)
|
49
|
+
expect(list.to_a).to eq([3, 4])
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'should shift correctly from a list containing 1 element' do
|
53
|
+
list = LinkedList.new(1)
|
54
|
+
expect(list.shift).to eq(1)
|
55
|
+
expect(list.to_a).to eq([])
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'should raise an error when shifting from an empty list' do
|
59
|
+
list = LinkedList.new
|
60
|
+
expect(->{ list.shift }).to raise_error(RuntimeError)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|