trick_bag 0.37.0 → 0.38.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/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
|