trick_bag 0.30.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|