trick_bag 0.37.0 → 0.38.0
Sign up to get free protection for your applications and to get access to all the features.
- data/RELEASE_NOTES.md +11 -0
- data/lib/trick_bag/collections/linked_list.rb +25 -12
- data/lib/trick_bag/enumerables/buffered_enumerable.rb +26 -9
- data/lib/trick_bag/enumerables/compound_enumerable.rb +2 -0
- data/lib/trick_bag/enumerables/endless_last_enumerable.rb +18 -15
- data/lib/trick_bag/enumerables/file_line_reader.rb +18 -3
- data/lib/trick_bag/enumerables/filtered_enumerable.rb +9 -5
- data/lib/trick_bag/formatters/formatters.rb +17 -2
- data/lib/trick_bag/io/temp_files.rb +8 -0
- data/lib/trick_bag/io/text_mode_status_updater.rb +32 -16
- data/lib/trick_bag/meta/classes.rb +23 -3
- data/lib/trick_bag/numeric/multi_counter.rb +12 -0
- data/lib/trick_bag/numeric/start_and_max.rb +8 -3
- data/lib/trick_bag/numeric/totals.rb +10 -4
- data/lib/trick_bag/operators/operators.rb +2 -0
- data/lib/trick_bag/timing/timing.rb +2 -0
- data/lib/trick_bag/validations/hash_validations.rb +2 -1
- data/lib/trick_bag/version.rb +1 -1
- data/spec/trick_bag/collections/linked_list_spec.rb +14 -8
- data/spec/trick_bag/enumerables/buffered_enumerable_spec.rb +4 -3
- data/spec/trick_bag/enumerables/compound_enumerable_spec.rb +4 -0
- data/spec/trick_bag/enumerables/endless_last_enumerable_spec.rb +18 -2
- data/spec/trick_bag/enumerables/file_line_reader_spec.rb +3 -0
- data/spec/trick_bag/enumerables/filtered_enumerable_spec.rb +5 -0
- data/spec/trick_bag/validations/hashes_validations_spec.rb +1 -1
- metadata +2 -2
data/RELEASE_NOTES.md
CHANGED
@@ -1,3 +1,14 @@
|
|
1
|
+
## v0.38.0
|
2
|
+
|
3
|
+
* Added documentation.
|
4
|
+
* Changed string returned by missing_hash_entries_as_string to be array.inspect.
|
5
|
+
* TextModeStatusUpdater.print now calls to_s on lambda's return value.
|
6
|
+
* Modified EndlessLastEnumerable so it could be initialized with any Enumerable, not only an Array.
|
7
|
+
* Added documentation, tests that enumerators return enumerables when yield is called without a block.
|
8
|
+
* Changed FilteredEnumerable default filter to be a lambda that always returns true.
|
9
|
+
* Added LinkedList#to_ary.
|
10
|
+
|
11
|
+
|
1
12
|
## v0.37.0
|
2
13
|
|
3
14
|
* Fixed: 'os' gem dependency was not properly specified in gemspec.
|
@@ -3,7 +3,6 @@ module Collections
|
|
3
3
|
|
4
4
|
# Linked List based on git@github.com:neilparikh/ruby-linked-list.git,
|
5
5
|
# but modified somewhat.
|
6
|
-
|
7
6
|
class LinkedList
|
8
7
|
|
9
8
|
class Node
|
@@ -18,18 +17,21 @@ class LinkedList
|
|
18
17
|
attr_accessor :first
|
19
18
|
attr_reader :length
|
20
19
|
|
20
|
+
|
21
|
+
# @param items items with which to initialize the list
|
21
22
|
def initialize(*items)
|
22
23
|
@length = items.length
|
23
24
|
@first = Node.new(items.shift)
|
24
25
|
items.each { |item| push(item) }
|
25
26
|
end
|
26
27
|
|
27
|
-
|
28
|
-
#
|
28
|
+
|
29
|
+
# @param value value to add to end of list
|
30
|
+
# @return self
|
29
31
|
def push(value)
|
30
32
|
node = Node.new(value)
|
31
33
|
current_node = @first
|
32
|
-
while current_node.next
|
34
|
+
while current_node.next
|
33
35
|
current_node = current_node.next
|
34
36
|
end
|
35
37
|
current_node.next = node
|
@@ -37,8 +39,9 @@ class LinkedList
|
|
37
39
|
self
|
38
40
|
end
|
39
41
|
|
40
|
-
|
41
|
-
#
|
42
|
+
|
43
|
+
# Returns the last element from the list and removes it
|
44
|
+
# @return the last element's value
|
42
45
|
def pop
|
43
46
|
case(@length)
|
44
47
|
|
@@ -64,8 +67,9 @@ class LinkedList
|
|
64
67
|
end
|
65
68
|
|
66
69
|
|
67
|
-
#
|
68
|
-
#
|
70
|
+
# Adds a value to the beginning of the list
|
71
|
+
# @param value value to add to beginning of list
|
72
|
+
# @return self
|
69
73
|
def unshift(value)
|
70
74
|
node = Node.new(value, @first)
|
71
75
|
@first = node
|
@@ -73,16 +77,19 @@ class LinkedList
|
|
73
77
|
self
|
74
78
|
end
|
75
79
|
|
76
|
-
|
77
|
-
#
|
80
|
+
|
81
|
+
# Removes the first element from the list
|
82
|
+
# @return the first element's value
|
78
83
|
def shift
|
79
84
|
raise "List is empty" if @length < 1
|
80
|
-
|
85
|
+
return_value = @first.value
|
81
86
|
@first = @first.next
|
82
87
|
@length -= 1
|
83
|
-
|
88
|
+
return_value
|
84
89
|
end
|
85
90
|
|
91
|
+
|
92
|
+
# @return the values in this list as an array
|
86
93
|
def to_a
|
87
94
|
current_node = @first
|
88
95
|
array = []
|
@@ -92,6 +99,12 @@ class LinkedList
|
|
92
99
|
end
|
93
100
|
array
|
94
101
|
end
|
102
|
+
|
103
|
+
|
104
|
+
# @return the values in this list as an array
|
105
|
+
def to_ary
|
106
|
+
to_a
|
107
|
+
end
|
95
108
|
end
|
96
109
|
end
|
97
110
|
end
|
@@ -1,6 +1,20 @@
|
|
1
|
+
|
2
|
+
require 'trick_bag/meta/classes'
|
3
|
+
|
4
|
+
module TrickBag
|
5
|
+
module Enumerables
|
6
|
+
|
1
7
|
# Provides the plumbing for an enumerator that, in order to serve objects in each(),
|
2
8
|
# fetches them in chunks.
|
3
9
|
#
|
10
|
+
# This class knows nothing about how to fetch anything; that behavior is provided
|
11
|
+
# by either subclassing this class, or calling .create_with_lambdas and passing
|
12
|
+
# a lambda that knows how to do that.
|
13
|
+
#
|
14
|
+
# Also supported is an optional fetch notification, a method or lambda that will
|
15
|
+
# be called whenever a fetch is done. This can be useful to update counters,
|
16
|
+
# provide user feedback (e.g. a progress bar)
|
17
|
+
#
|
4
18
|
# This is useful, for example, in network requests, when multiple requests can be sent
|
5
19
|
# one immediately after another, and the responses can be collected as a group,
|
6
20
|
# for improved performance.
|
@@ -10,15 +24,10 @@
|
|
10
24
|
# needlessly copying arrays,
|
11
25
|
# and to eliminate the need for garbage collecting many array objects
|
12
26
|
# (though the latter is not that important).
|
13
|
-
|
14
|
-
require 'trick_bag/meta/classes'
|
15
|
-
|
16
|
-
module TrickBag
|
17
|
-
module Enumerables
|
18
|
-
|
19
27
|
class BufferedEnumerable
|
20
28
|
|
21
29
|
include Enumerable
|
30
|
+
|
22
31
|
extend ::TrickBag::Meta::Classes
|
23
32
|
|
24
33
|
attr_accessor :fetcher, :fetch_notifier
|
@@ -28,6 +37,10 @@ class BufferedEnumerable
|
|
28
37
|
attr_access :public, :private, :chunk_count, :fetch_count, :yield_count
|
29
38
|
|
30
39
|
|
40
|
+
# Creates an instance with lambdas for fetch and fetch notify behaviors.
|
41
|
+
# @param chunk_size the maximum number of objects to be buffered
|
42
|
+
# @param fetcher lambda to be called to fetch to fill the buffer
|
43
|
+
# @param fetch_notifier lambda to be called to when a fetch is done
|
31
44
|
def self.create_with_lambdas(chunk_size, fetcher, fetch_notifier)
|
32
45
|
instance = self.new(chunk_size)
|
33
46
|
instance.fetcher = fetcher
|
@@ -35,9 +48,9 @@ class BufferedEnumerable
|
|
35
48
|
instance
|
36
49
|
end
|
37
50
|
|
38
|
-
# @param fetcher
|
39
|
-
# @param chunk_size
|
40
|
-
# @param fetch_notifier
|
51
|
+
# @param fetcher lambda to be called to fetch to fill the buffer
|
52
|
+
# @param chunk_size the maximum number of objects to be buffered
|
53
|
+
# @param fetch_notifier lambda to be called to when a fetch is done
|
41
54
|
# in case the caller wants to receive notification, update counters, etc.
|
42
55
|
# It's passed the array of objects just fetched, whose size may be
|
43
56
|
# less than chunk size.
|
@@ -64,6 +77,10 @@ class BufferedEnumerable
|
|
64
77
|
end
|
65
78
|
|
66
79
|
|
80
|
+
# Enumerable.each method.
|
81
|
+
# Note that, like all Enumerable.each methods, if you pass it without a block,
|
82
|
+
# it will return an enumerator, and any cleanup that would normally be done
|
83
|
+
# after each's loop has completed will not happen.
|
67
84
|
def each
|
68
85
|
return to_enum unless block_given?
|
69
86
|
|
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
1
3
|
module TrickBag
|
2
4
|
module Enumerables
|
3
5
|
|
@@ -23,16 +25,16 @@ module Enumerables
|
|
23
25
|
|
24
26
|
include Enumerable
|
25
27
|
|
26
|
-
attr_reader :
|
28
|
+
attr_reader :inner_enumerable
|
27
29
|
|
28
|
-
def initialize(
|
29
|
-
@
|
30
|
-
when
|
31
|
-
|
30
|
+
def initialize(enumerable_or_number)
|
31
|
+
@inner_enumerable = case enumerable_or_number
|
32
|
+
when Enumerable
|
33
|
+
enumerable_or_number
|
32
34
|
when Numeric
|
33
|
-
|
35
|
+
Array(enumerable_or_number)
|
34
36
|
else
|
35
|
-
raise RuntimeError.new("Unsupported data type (#{
|
37
|
+
raise RuntimeError.new("Unsupported data type (#{enumerable_or_number.class}.")
|
36
38
|
end
|
37
39
|
@pos = 0
|
38
40
|
end
|
@@ -40,20 +42,21 @@ module Enumerables
|
|
40
42
|
|
41
43
|
def each
|
42
44
|
return to_enum unless block_given?
|
45
|
+
last_number = nil
|
46
|
+
|
47
|
+
inner_enumerable.each do |number|
|
48
|
+
yield(number)
|
49
|
+
last_number = number
|
50
|
+
end
|
51
|
+
|
43
52
|
loop do
|
44
|
-
|
45
|
-
value = @array.last
|
46
|
-
else
|
47
|
-
value = @array[@pos]
|
48
|
-
@pos += 1
|
49
|
-
end
|
50
|
-
yield(value)
|
53
|
+
yield(last_number)
|
51
54
|
end
|
52
55
|
end
|
53
56
|
|
54
57
|
|
55
58
|
def ==(other) # mostly for testing
|
56
|
-
other.is_a?(self.class) && other.
|
59
|
+
other.is_a?(self.class) && other.inner_enumerable == inner_enumerable
|
57
60
|
end
|
58
61
|
end
|
59
62
|
end
|
@@ -9,6 +9,13 @@ module Enumerables
|
|
9
9
|
#
|
10
10
|
# If calling each without a block to get an enumerator, call enumerator.close when done
|
11
11
|
# if not all values have been exhausted, so that the input file will be closed.
|
12
|
+
#
|
13
|
+
# Supports specifying a starting position and maximum count. For example
|
14
|
+
# if the starting position is 2, then the first 2 valid lines will be
|
15
|
+
# discarded, and yielding will begin with the next valid line.
|
16
|
+
#
|
17
|
+
# Similarly, specifying a maximum count will cause yielding to end after
|
18
|
+
# max_count objects have been yielded.
|
12
19
|
class FileLineReader
|
13
20
|
|
14
21
|
include Enumerable
|
@@ -24,22 +31,27 @@ class FileLineReader
|
|
24
31
|
@start_and_max = TrickBag::Numeric::StartAndMax.new(start_pos, max_count)
|
25
32
|
end
|
26
33
|
|
27
|
-
|
34
|
+
# @return the position of the first valid object to be yielded.
|
28
35
|
def start_pos
|
29
36
|
start_and_max.start_pos
|
30
37
|
end
|
31
38
|
|
32
39
|
|
40
|
+
# @return the maximum number of objects to be yielded
|
33
41
|
def max_count
|
34
42
|
start_and_max.max_count
|
35
43
|
end
|
36
44
|
|
37
45
|
|
46
|
+
# @param line the line to test
|
47
|
+
# @return whether or not this line is valid (eligible for yielding),
|
48
|
+
# specifically not empty and not a comment
|
38
49
|
def line_valid?(line)
|
39
50
|
! (line.empty? || /^#/ === line)
|
40
51
|
end
|
41
52
|
|
42
53
|
|
54
|
+
# Closes the file if it is not null and has not already been closed
|
43
55
|
def close_file(file)
|
44
56
|
if file && (! file.closed?)
|
45
57
|
file.close
|
@@ -47,12 +59,15 @@ class FileLineReader
|
|
47
59
|
end
|
48
60
|
|
49
61
|
|
50
|
-
# Any
|
51
|
-
#
|
62
|
+
# Any enumerator returned should have a close method to close the file
|
63
|
+
# from which lines are read. This method gets an enumerator from the superclass,
|
64
|
+
# and adds a 'file' attribute and a close method to it.
|
52
65
|
def to_enum(file)
|
53
66
|
enumerator = super()
|
54
67
|
enumerator.instance_variable_set(:@file, file)
|
55
68
|
|
69
|
+
# This method is defined on the instance of the new enumerator.
|
70
|
+
# It closes the input file.
|
56
71
|
def enumerator.close
|
57
72
|
if @file && (! @file.closed?)
|
58
73
|
@file.close
|
@@ -12,19 +12,23 @@ class FilteredEnumerable
|
|
12
12
|
|
13
13
|
include Enumerable
|
14
14
|
|
15
|
-
attr_reader :
|
15
|
+
attr_reader :wrapped_enumerator
|
16
16
|
attr_accessor :filter
|
17
17
|
|
18
|
-
|
19
|
-
|
18
|
+
# @param wrapped_enumerator the enumerator to filter
|
19
|
+
# @param filter a lambda that returns whether or not to yield a given object,
|
20
|
+
# defaults to lambda always returning true
|
21
|
+
def initialize(wrapped_enumerator, filter = ->(_) { true })
|
22
|
+
@wrapped_enumerator = wrapped_enumerator
|
20
23
|
@filter = filter
|
21
24
|
end
|
22
25
|
|
26
|
+
# Standard Enumerable.each; returns an Enumerator if called without a block
|
23
27
|
def each
|
24
28
|
return to_enum unless block_given?
|
25
29
|
|
26
|
-
|
27
|
-
yield thing if filter.
|
30
|
+
wrapped_enumerator.each do |thing|
|
31
|
+
yield thing if filter.(thing)
|
28
32
|
end
|
29
33
|
end
|
30
34
|
end
|
@@ -5,13 +5,20 @@ module Formatters
|
|
5
5
|
|
6
6
|
module_function
|
7
7
|
|
8
|
+
# Formats a number of seconds as a succinct string, with days, hours,
|
9
|
+
# minutes, and seconds. Examples:
|
10
|
+
#
|
11
|
+
# duration_to_s(1_000_000) => "11 d, 13 h, 46 m, 40 s"
|
12
|
+
# duration_to_s(1_000) => "16 m, 40 s"
|
13
|
+
# duration_to_s(-1_000) => "-16 m, 40 s"
|
14
|
+
# duration_to_s(1.234567) => "1.234567 s"
|
15
|
+
# duration_to_s(1000.234567) => "16 m, 40.23456699999997 s"
|
8
16
|
def duration_to_s(seconds)
|
9
17
|
|
10
18
|
seconds_in_minute = 60
|
11
19
|
seconds_in_hour = 60 * seconds_in_minute
|
12
20
|
seconds_in_day = 24 *seconds_in_hour
|
13
21
|
|
14
|
-
seconds = seconds.to_i
|
15
22
|
str = ''
|
16
23
|
|
17
24
|
if seconds < 0
|
@@ -53,6 +60,9 @@ module Formatters
|
|
53
60
|
end
|
54
61
|
|
55
62
|
|
63
|
+
# Returns a timestamp string suitable for use in filespecs,
|
64
|
+
# whose string sort order will be the same as a chronological sort would be.
|
65
|
+
# @param datetime the date to format, defaults to DateTime.now
|
56
66
|
def timestamp(datetime = DateTime.now)
|
57
67
|
datetime.strftime('%Y-%m-%d_%H-%M-%S')
|
58
68
|
end
|
@@ -60,7 +70,12 @@ module Formatters
|
|
60
70
|
|
61
71
|
# Replaces all occurrences of marker with the current date/time in
|
62
72
|
# YYYYMMDD-HHMMSS format.
|
63
|
-
|
73
|
+
#
|
74
|
+
# Useful for creating filespecs with static content that will differ by date,
|
75
|
+
# for example:
|
76
|
+
#
|
77
|
+
# replace_with_timestamp('my-app-result-{dt}.txt') => "my-app-result-2014-03-24_15-25-57.txt"
|
78
|
+
def replace_with_timestamp(string, marker = '{dt}', datetime = DateTime.now)
|
64
79
|
string.gsub(marker, timestamp(datetime))
|
65
80
|
end
|
66
81
|
|
@@ -4,7 +4,15 @@ module TempFiles
|
|
4
4
|
|
5
5
|
module_function
|
6
6
|
|
7
|
+
# Creates a temporary file containing the specified text,
|
8
|
+
# passes its filespec to the passed block, then deletes the file.
|
9
|
+
#
|
10
|
+
# @param text the text to write to the temporary file
|
11
|
+
# @param file_prefix optional prefix for the temporary file's name
|
12
|
+
# @yield filespec of the temporary file
|
7
13
|
def self.file_containing(text, file_prefix = '')
|
14
|
+
raise "This method must be called with a code block." unless block_given?
|
15
|
+
|
8
16
|
filespec = nil
|
9
17
|
begin
|
10
18
|
Tempfile.open(file_prefix) do |file|
|
@@ -4,6 +4,11 @@ module Io
|
|
4
4
|
# Updates the terminal line with text, erasing the original content and displaying at the same place.
|
5
5
|
# Uses ANSI escape sequences for cursor positioning and clearing
|
6
6
|
# (see http://www.oldlinux.org/Linux.old/Ref-docs/ASCII/ANSI%20Escape%20Sequences.htm).
|
7
|
+
#
|
8
|
+
# Example:
|
9
|
+
#
|
10
|
+
# updater = TrickBag::Io::TextModeStatusUpdater.new(->{ Time.now })
|
11
|
+
# 5.times { updater.print; sleep(1) }
|
7
12
|
class TextModeStatusUpdater
|
8
13
|
|
9
14
|
|
@@ -14,6 +19,33 @@ class TextModeStatusUpdater
|
|
14
19
|
@first_time = true
|
15
20
|
end
|
16
21
|
|
22
|
+
|
23
|
+
# Causes the text generator lambda to be called, calls to_s on its result,
|
24
|
+
# and outputs the resulting text to the output stream, moving the cursor
|
25
|
+
# left to the beginning of the previous call's output if not the first call to print.
|
26
|
+
#
|
27
|
+
# Since this method uses ASCII escape sequences that would look messy in a file,
|
28
|
+
# this method will silently return if the output stream is not a TTY, unless
|
29
|
+
# @force_output_non_tty has been set to true.
|
30
|
+
def print
|
31
|
+
|
32
|
+
# If output is being redirected, don't print anything; it will look like garbage;
|
33
|
+
# But if output was forced (e.g. to write to a string), then allow it.
|
34
|
+
return unless @outstream.tty? || @force_output_non_tty
|
35
|
+
|
36
|
+
if @first_time
|
37
|
+
@first_time = false
|
38
|
+
else
|
39
|
+
@outstream.print(move_cursor_left_text(@prev_text_length))
|
40
|
+
end
|
41
|
+
text = @text_generator.().to_s
|
42
|
+
@prev_text_length = text.length
|
43
|
+
@outstream.print(clear_to_end_of_line_text + text)
|
44
|
+
end
|
45
|
+
|
46
|
+
# The following methods are for cursor placement and text clearing:
|
47
|
+
private
|
48
|
+
|
17
49
|
def clear_to_end_of_line_text
|
18
50
|
"\x1b[2K"
|
19
51
|
end
|
@@ -38,22 +70,6 @@ class TextModeStatusUpdater
|
|
38
70
|
"\x1b[#{num_chars}@"
|
39
71
|
end
|
40
72
|
|
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
73
|
end
|
58
74
|
end
|
59
75
|
end
|
@@ -15,13 +15,14 @@ module Classes
|
|
15
15
|
#
|
16
16
|
# attr_access(:private, :none, :foo, :bar)
|
17
17
|
#
|
18
|
-
# will create accessors for @foo and @bar,
|
19
|
-
# but will not create any
|
18
|
+
# will create readers (aka accessors or getters) for @foo and @bar,
|
19
|
+
# and make them private, but will not create any writers
|
20
|
+
# (aka mutators or setters).
|
20
21
|
def attr_access(read_access, write_access, *attrs)
|
21
22
|
validate_input = -> do
|
22
23
|
[read_access, write_access].each do |access|
|
23
24
|
unless VALID_ACCESS_MODES.include?(access)
|
24
|
-
raise "Access mode must be one of
|
25
|
+
raise "Access mode must be one of #{VALID_ACCESS_MODES.inspect}]."
|
25
26
|
end
|
26
27
|
end
|
27
28
|
end
|
@@ -41,41 +42,60 @@ module Classes
|
|
41
42
|
end
|
42
43
|
|
43
44
|
|
45
|
+
# Creates private readers and no writers for the specified attributes.
|
44
46
|
def private_attr_reader(*attrs)
|
45
47
|
attr_access(:private, :none, *attrs)
|
46
48
|
end
|
47
49
|
|
48
50
|
|
51
|
+
# Creates private writers and no readers for the specified attributes.
|
49
52
|
def private_attr_writer(*attrs)
|
50
53
|
attr_access(:none, :private, *attrs)
|
51
54
|
end
|
52
55
|
|
53
56
|
|
57
|
+
# Creates private readers and writers for the specified attributes.
|
54
58
|
def private_attr_accessor(*attrs)
|
55
59
|
attr_access(:private, :private, *attrs)
|
56
60
|
end
|
57
61
|
|
58
62
|
|
63
|
+
# Creates protected readers and no writers for the specified attributes.
|
59
64
|
def protected_attr_reader(*attrs)
|
60
65
|
attr_access(:protected, :none, *attrs)
|
61
66
|
end
|
62
67
|
|
63
68
|
|
69
|
+
# Creates protected writers and no readers for the specified attributes.
|
64
70
|
def protected_attr_writer(*attrs)
|
65
71
|
attr_access(:none, :protected, *attrs)
|
66
72
|
end
|
67
73
|
|
68
74
|
|
75
|
+
# Creates protected readers and writers for the specified attributes.
|
69
76
|
def protected_attr_accessor(*attrs)
|
70
77
|
attr_access(:protected, :protected, *attrs)
|
71
78
|
end
|
72
79
|
|
73
80
|
|
81
|
+
# Returns whether or not the specified name is a defined class.
|
82
|
+
# @param class class name (a string)
|
83
|
+
# @param scope scope in which the class would be defined, defaults to Object
|
84
|
+
# (e.g. could be MyModule::InnerModule)
|
85
|
+
#
|
86
|
+
# e.g. module M; class X; end; end; class?('X', M) => true
|
74
87
|
def class?(name, scope = Object)
|
75
88
|
scope.const_defined?(name) && scope.const_get(name).is_a?(Class)
|
76
89
|
end; module_function :class?
|
77
90
|
|
78
91
|
|
92
|
+
# Undefines a class; useful for unit testing code that manipulates classes.
|
93
|
+
#
|
94
|
+
# @param name name of class to undefine
|
95
|
+
# @param scope scope in which the class is defined, defaults to Object
|
96
|
+
#
|
97
|
+
# e.g. module M; class X; end; end; class?('X', M) => true
|
98
|
+
# undef_class('X', M); class?('X', M) => false
|
79
99
|
def undef_class(name, scope = Object)
|
80
100
|
class_existed = class?(name, scope)
|
81
101
|
scope.send(:remove_const, name) if class_existed
|
@@ -7,35 +7,47 @@ class MultiCounter
|
|
7
7
|
|
8
8
|
attr_accessor :name
|
9
9
|
|
10
|
+
# Creates a multicounter.
|
11
|
+
#
|
12
|
+
# @param name optional name for printing in to_s
|
10
13
|
def initialize(name = '')
|
11
14
|
@name = name
|
12
15
|
@counts = Hash.new(0)
|
13
16
|
end
|
14
17
|
|
18
|
+
# Adds keys in the passed enumerable to this counter.
|
15
19
|
def add_keys(keys)
|
16
20
|
keys.each { |key| @counts[key] = 0 }
|
17
21
|
end
|
18
22
|
|
23
|
+
# Increments the value corresponding to the specified key.
|
19
24
|
def increment(key)
|
20
25
|
@counts[key] += 1
|
21
26
|
end
|
22
27
|
|
28
|
+
# Returns the count for the specified key.
|
23
29
|
def [](key)
|
24
30
|
@counts[key]
|
25
31
|
end
|
26
32
|
|
33
|
+
# Returns this counter's keys.
|
27
34
|
def keys
|
28
35
|
@counts.keys
|
29
36
|
end
|
30
37
|
|
38
|
+
# Returns whether or not the specified key exists in this counter.
|
31
39
|
def key_exists?(key)
|
32
40
|
keys.include?(key)
|
33
41
|
end
|
34
42
|
|
43
|
+
# Creates a hash whose keys are this counter's keys, and whose values are
|
44
|
+
# their corresponding values.
|
35
45
|
def to_hash
|
36
46
|
@counts.clone
|
37
47
|
end
|
38
48
|
|
49
|
+
# Returns a string representing this counter, including its values,
|
50
|
+
# and, if specified, its optional name.
|
39
51
|
def to_s
|
40
52
|
"#{self.class} '#{name}': #{@counts.to_s}"
|
41
53
|
end
|
@@ -2,7 +2,7 @@ module TrickBag
|
|
2
2
|
module Numeric
|
3
3
|
|
4
4
|
# Like a Range, but includes useful functions with understandable names
|
5
|
-
# that account for the
|
5
|
+
# that account for the limits or absences of limits.
|
6
6
|
class StartAndMax
|
7
7
|
|
8
8
|
attr_reader :start_pos, :max_count
|
@@ -11,22 +11,27 @@ module Numeric
|
|
11
11
|
# @param max_count - the maximum number of records to be served by each(); or :infinite
|
12
12
|
def initialize(start_pos = :first, max_count = :infinite)
|
13
13
|
@start_pos = ([nil, :first].include?(start_pos) || start_pos <= 0) ? :first : start_pos
|
14
|
-
@max_count = ([nil, :infinite].include?(max_count)
|
14
|
+
@max_count = ([nil, :infinite].include?(max_count) || max_count <= 0) ? :infinite : max_count
|
15
15
|
end
|
16
16
|
|
17
17
|
|
18
|
+
# If a starting position has been specified, returns whether or not the specified number
|
19
|
+
# >= that position. Else, returns true unconditionally.
|
18
20
|
def start_position_reached?(num)
|
19
21
|
@start_pos == :first || num >= @start_pos
|
20
22
|
end
|
21
23
|
|
22
24
|
|
25
|
+
# If a maximum count has been specified, returns whether or not the specified number
|
26
|
+
# >= that count. Else, returns false unconditionally.
|
23
27
|
def max_count_reached?(count)
|
24
28
|
@max_count != :infinite && (count >= @max_count)
|
25
29
|
end
|
26
30
|
|
27
31
|
|
32
|
+
# Returns string representation including starting position and maximum count.
|
28
33
|
def to_s
|
29
|
-
"#{self.class}: start position=#{start_pos}, max count=#{max_count}"
|
34
|
+
"#{self.class}: start position=#{start_pos.inspect}, max count=#{max_count.inspect}"
|
30
35
|
end
|
31
36
|
|
32
37
|
end
|
@@ -6,6 +6,8 @@ module Totals
|
|
6
6
|
|
7
7
|
# @inputs an enumerable of numbers
|
8
8
|
# @return a collection containing the corresponding fractions of total of those numbers
|
9
|
+
#
|
10
|
+
# e.g.: map_fraction_of_total [10,20,30] => [0.16666666666666666, 0.3333333333333333, 0.5]
|
9
11
|
def map_fraction_of_total(inputs)
|
10
12
|
return [] if inputs.size == 0
|
11
13
|
sum = Float(inputs.inject(:+))
|
@@ -15,6 +17,8 @@ module Totals
|
|
15
17
|
|
16
18
|
# @inputs an enumerable of numbers
|
17
19
|
# @return a collection containing the corresponding percents of total of those numbers
|
20
|
+
#
|
21
|
+
# e.g. map_percent_of_total [10,20,30] => [16.666666666666664, 33.33333333333333, 50.0]
|
18
22
|
def map_percent_of_total(inputs)
|
19
23
|
map_fraction_of_total(inputs).map { |n| n * 100 }
|
20
24
|
end
|
@@ -22,8 +26,9 @@ module Totals
|
|
22
26
|
|
23
27
|
# Given a hash whose values are numbers, produces a new hash with the same keys
|
24
28
|
# as the original hash, but whose values are the % of total.
|
25
|
-
#
|
26
|
-
# { foo: 10.0, bar: 20.0, baz: 30.0, razz: 40.0 }
|
29
|
+
#
|
30
|
+
# Ex: fraction_of_total_hash({ foo: 10.0, bar: 20.0, baz: 30.0, razz: 40.0 })
|
31
|
+
# => {:foo=>0.1, :bar=>0.2, :baz=>0.3, :razz=>0.4}
|
27
32
|
def fraction_of_total_hash(the_hash)
|
28
33
|
new_hash = percent_of_total_hash(the_hash)
|
29
34
|
new_hash.keys.each do |key|
|
@@ -35,8 +40,9 @@ module Totals
|
|
35
40
|
|
36
41
|
# Given a hash whose values are numbers, produces a new hash with the same keys
|
37
42
|
# as the original hash, but whose values are the % of total.
|
38
|
-
#
|
39
|
-
# { foo: 10.0, bar: 20.0, baz: 30.0, razz: 40.0 }
|
43
|
+
#
|
44
|
+
# Ex: percent_of_total_hash({ foo: 10.0, bar: 20.0, baz: 30.0, razz: 40.0 })
|
45
|
+
# => {:foo=>10.0, :bar=>20.0, :baz=>30.0, :razz=>40.0}
|
40
46
|
def percent_of_total_hash(the_hash)
|
41
47
|
sum = Float(the_hash.values.inject(:+))
|
42
48
|
keys = the_hash.keys
|
@@ -4,6 +4,8 @@ module Operators
|
|
4
4
|
module_function
|
5
5
|
|
6
6
|
# Returns whether or not all passed values are equal
|
7
|
+
#
|
8
|
+
# Ex: multi_eq(1, 1, 1, 2) => false; multi_eq(1, 1, 1, 1) => true
|
7
9
|
def multi_eq(*values)
|
8
10
|
values = values.first if values.is_a?(Array) && values.size == 1
|
9
11
|
raise "Must be called with at least 2 parameters" if values.size < 2
|
@@ -14,6 +14,8 @@ module Timing
|
|
14
14
|
# that returns a truthy value that indicates no further retries are necessary
|
15
15
|
# @param sleep_interval number of seconds (fractions ok) to wait between tries
|
16
16
|
# @param timeout_secs maximum number of seconds (fractions ok) during which to retry
|
17
|
+
#
|
18
|
+
# Ex: TrickBag::Timing.retry_until_true_or_timeout(->{false}, 1.0, 5)
|
17
19
|
def retry_until_true_or_timeout(
|
18
20
|
predicate, sleep_interval, timeout_secs, output_stream = $stdout)
|
19
21
|
|
@@ -14,11 +14,12 @@ module HashValidations
|
|
14
14
|
keys.reject { |key| the_hash.keys.include?(key) }
|
15
15
|
end
|
16
16
|
|
17
|
+
|
17
18
|
# Looks to see which keys, if any, are missing from the hash.
|
18
19
|
# @return nil if none missing, else comma separated string of missing keys.
|
19
20
|
def missing_hash_entries_as_string(the_hash, *keys)
|
20
21
|
missing_keys = missing_hash_entries(the_hash, *keys)
|
21
|
-
missing_keys.empty? ? nil : missing_keys.
|
22
|
+
missing_keys.empty? ? nil : missing_keys.inspect
|
22
23
|
end
|
23
24
|
|
24
25
|
# Checks to see that all passed keys are present in the hash.
|
data/lib/trick_bag/version.rb
CHANGED
@@ -8,9 +8,11 @@ module Collections
|
|
8
8
|
|
9
9
|
describe LinkedList do
|
10
10
|
|
11
|
+
let(:sample_array) { [1, 2, 3] }
|
12
|
+
let(:sample_list) { LinkedList.new(*sample_array) }
|
13
|
+
|
11
14
|
specify 'to_a should return an array equal to the array with which it was initialized' do
|
12
|
-
|
13
|
-
expect(LinkedList.new(*array).to_a).to eq(array)
|
15
|
+
expect(LinkedList.new(*sample_array).to_a).to eq(sample_array)
|
14
16
|
end
|
15
17
|
|
16
18
|
it 'should push correctly' do
|
@@ -20,9 +22,8 @@ describe LinkedList do
|
|
20
22
|
end
|
21
23
|
|
22
24
|
it 'should pop from a list of multiple elements correctly' do
|
23
|
-
|
24
|
-
expect(
|
25
|
-
expect(list.to_a).to eq([1, 2])
|
25
|
+
expect(sample_list.pop).to eq(3)
|
26
|
+
expect(sample_list.to_a).to eq([1, 2])
|
26
27
|
end
|
27
28
|
|
28
29
|
it 'should pop from a list of 1 element correctly' do
|
@@ -44,9 +45,8 @@ describe LinkedList do
|
|
44
45
|
end
|
45
46
|
|
46
47
|
it 'should shift correctly from a list containing multiple elements' do
|
47
|
-
|
48
|
-
expect(
|
49
|
-
expect(list.to_a).to eq([3, 4])
|
48
|
+
expect(sample_list.shift).to eq(1)
|
49
|
+
expect(sample_list.to_a).to eq([2, 3])
|
50
50
|
end
|
51
51
|
|
52
52
|
it 'should shift correctly from a list containing 1 element' do
|
@@ -59,6 +59,12 @@ describe LinkedList do
|
|
59
59
|
list = LinkedList.new
|
60
60
|
expect(->{ list.shift }).to raise_error(RuntimeError)
|
61
61
|
end
|
62
|
+
|
63
|
+
specify 'to_ary should return an array identical to that returned by to_a' do
|
64
|
+
a = sample_list.to_a
|
65
|
+
ary = sample_list.to_ary
|
66
|
+
expect(ary).to eq(a)
|
67
|
+
end
|
62
68
|
end
|
63
69
|
end
|
64
70
|
end
|
@@ -73,6 +73,7 @@ module Enumerables
|
|
73
73
|
(1..10).each do |n|
|
74
74
|
expect(enumerator.next).to eq(n)
|
75
75
|
end
|
76
|
+
expect(enumerator).to be_a(Enumerator)
|
76
77
|
expect(enumerable.chunk_fetch_calls).to eq(3)
|
77
78
|
expect(enumerable.object_count).to eq(12)
|
78
79
|
::TrickBag::Meta::Classes.undef_class(:BufferedEnumerableSubclass, TrickBag)
|
@@ -87,12 +88,12 @@ module Enumerables
|
|
87
88
|
|
88
89
|
def initialize(chunk_size, array)
|
89
90
|
super(chunk_size)
|
90
|
-
@
|
91
|
+
@inner_enumerable = array
|
91
92
|
end
|
92
93
|
|
93
94
|
def fetch
|
94
|
-
num_times = [chunk_size, @
|
95
|
-
num_times.times { @data << @
|
95
|
+
num_times = [chunk_size, @inner_enumerable.size].min
|
96
|
+
num_times.times { @data << @inner_enumerable.shift }
|
96
97
|
end
|
97
98
|
end
|
98
99
|
end
|
@@ -75,6 +75,10 @@ module Enumerables
|
|
75
75
|
it "provides the 'take' method" do
|
76
76
|
expect(->{ CompoundEnumerable.array_enumerable([1, 2, 3]).take(2)}).not_to raise_error
|
77
77
|
end
|
78
|
+
|
79
|
+
it 'returns an Enumerator when each is called without a block' do
|
80
|
+
expect(CompoundEnumerable.array_enumerable([1, 2, 3]).each).to be_a(Enumerator)
|
81
|
+
end
|
78
82
|
end
|
79
83
|
end
|
80
84
|
|
@@ -29,10 +29,10 @@ module TrickBag::Enumerables
|
|
29
29
|
it "should return the original array and then repeat the last" do
|
30
30
|
array = (0..7).to_a
|
31
31
|
e = EndlessLastEnumerable.new(array).each
|
32
|
-
expect(e.take(
|
33
|
-
expect(e.take(12)).to eq([7] * 12)
|
32
|
+
expect(e.take(20)).to eq(array + ([7] * 12))
|
34
33
|
end
|
35
34
|
|
35
|
+
|
36
36
|
it "should return == for 2 instances created with the same array" do
|
37
37
|
array = (0..10).to_a
|
38
38
|
e1 = EndlessLastEnumerable.new(array)
|
@@ -46,6 +46,22 @@ module TrickBag::Enumerables
|
|
46
46
|
e2 = EndlessLastEnumerable.new([2, 1])
|
47
47
|
expect(e1).not_to eq(e2)
|
48
48
|
end
|
49
|
+
|
50
|
+
it 'returns an Enumerator when each is called without a block' do
|
51
|
+
expect(EndlessLastEnumerable.new([1]).each).to be_a(Enumerator)
|
52
|
+
end
|
53
|
+
|
54
|
+
context 'initializing with non-Array enumerables' do
|
55
|
+
specify 'it does not raise an error when initialized with a set' do
|
56
|
+
set = Set.new([1, 2])
|
57
|
+
expect(->() { EndlessLastEnumerable.new(set) }).not_to raise_error
|
58
|
+
end
|
59
|
+
|
60
|
+
specify 'it behaves correctly when initialized with a set' do
|
61
|
+
set = Set.new([1, 2])
|
62
|
+
expect(EndlessLastEnumerable.new(set).take(4)).to eq([1,2,2,2])
|
63
|
+
end
|
64
|
+
end
|
49
65
|
end
|
50
66
|
|
51
67
|
end
|
@@ -106,6 +106,9 @@ nbc.com
|
|
106
106
|
expect(get_lsof_lines.().size).to eq(0)
|
107
107
|
end
|
108
108
|
|
109
|
+
it 'returns an Enumerator when each is called without a block' do
|
110
|
+
expect(subject.each).to be_a(Enumerator)
|
111
|
+
end
|
109
112
|
|
110
113
|
specify "calling the enumerator's close method multiple times will not raise an error" do
|
111
114
|
enumerator = subject.each
|
@@ -30,6 +30,11 @@ module Enumerables
|
|
30
30
|
expect(enumerator.next).to eq(4)
|
31
31
|
expect(-> { enumerator.next }).to raise_error(StopIteration)
|
32
32
|
end
|
33
|
+
|
34
|
+
it 'returns an Enumerator when each is called without a block' do
|
35
|
+
expect(FilteredEnumerable.new([]).each).to be_a(Enumerator)
|
36
|
+
end
|
37
|
+
|
33
38
|
end
|
34
39
|
end
|
35
40
|
end
|
@@ -30,7 +30,7 @@ module Validations
|
|
30
30
|
it 'should return a correct string listing missing keys' do
|
31
31
|
h = { a: 1, c: 1, e: 1}
|
32
32
|
keys_to_test = [:e, :d, :c, :b, :a]
|
33
|
-
expect(missing_hash_entries_as_string(h, keys_to_test)).to eq(
|
33
|
+
expect(missing_hash_entries_as_string(h, keys_to_test)).to eq("[:d, :b]")
|
34
34
|
end
|
35
35
|
|
36
36
|
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: trick_bag
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.38.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2014-03-
|
12
|
+
date: 2014-03-24 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: os
|