bag.rb 0.1.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 9903f16469a6e4bb4ae8cafddb1a98ac309b26d575e5419bdfcb936df46a7818
4
+ data.tar.gz: d1d6915f5dbc2d2350b074238baafe99fb408d855cc2000fda8c24e6ccf73a61
5
+ SHA512:
6
+ metadata.gz: c88ac313f1270d67bf3b85cf09df979c490d14a0580e238d01c05317bb8ad8b738821fca8b83acba71af1895dafdbaf686faabdb947bbfc4f68d7df851d68874
7
+ data.tar.gz: 275713e0ed463c06e625ccda517028ebd4e478bb3e3a8937c76e317ee269be51b8d160ac6c90bd1a3b515bba4278cd63ec41f975e3c67c9e6f2641a71603815d
checksums.yaml.gz.sig ADDED
Binary file
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) Shannon Skipper
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,193 @@
1
+ # Bag
2
+
3
+ A pure Ruby implementation of a **Bag** (multiset), inspired by Smalltalk-80's Bag class.
4
+
5
+ A Bag is like a Set that remembers how many times you've added each element:
6
+
7
+ ```ruby
8
+ Set.new(%w[quartz quartz obsidian])
9
+ #=> #<Set: {"quartz", "obsidian"}>
10
+
11
+ Bag.new(%w[quartz quartz obsidian])
12
+ #=> #<Bag "quartz" => 2, "obsidian" => 1>
13
+
14
+ %w[quartz quartz obsidian].tally
15
+ #=> {"quartz" => 2, "obsidian" => 1}
16
+ ```
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ gem install bag.rb
22
+ ```
23
+
24
+ Or add to your Gemfile:
25
+
26
+ ```ruby
27
+ gem 'bag.rb'
28
+ ```
29
+
30
+ ## Usage
31
+
32
+ ```ruby
33
+ require 'bag'
34
+
35
+ words = Bag.new(%w[the quick brown fox jumps over the lazy dog])
36
+
37
+ words.count('the')
38
+ #=> 2
39
+ words.size
40
+ #=> 9
41
+ words.uniq_size
42
+ #=> 8
43
+
44
+ words << 'the' << 'the'
45
+ words.add('fox', 2)
46
+ words.remove('lazy')
47
+
48
+ words.sorted_by_count
49
+ #=> [['the', 4], ['fox', 3], ['quick', 1], ...]
50
+ words.sample
51
+ #=> 'the'
52
+ ```
53
+
54
+ ## API
55
+
56
+ ### Creation & Modification
57
+
58
+ ```ruby
59
+ bag = Bag.new
60
+ bag = Bag.new(%i[abyss void abyss chaos abyss])
61
+ #=> #<Bag abyss: 3, void: 1, chaos: 1>
62
+ bag = Bag.new(%i[abyss void abyss].tally)
63
+ #=> #<Bag abyss: 2, void: 1>
64
+
65
+ bag << 'nihil'
66
+ bag.add('entropy', 5)
67
+ bag.add('nihil', -1)
68
+
69
+ bag.remove('nihil')
70
+ bag.remove('nihil', 2)
71
+ bag.remove_all('entropy')
72
+ bag.clear
73
+
74
+ bag.count('abyss')
75
+ bag.include?('void')
76
+ #=> true
77
+ bag.empty?
78
+ #=> false
79
+ ```
80
+
81
+ ### Set Operations
82
+
83
+ ```ruby
84
+ bag1 = Bag.new(%i[phosphor phosphor glyph rune])
85
+ bag2 = Bag.new(%i[phosphor glyph glyph sigil])
86
+
87
+ bag1 + bag2
88
+ #=> #<Bag phosphor: 3, glyph: 3, rune: 1, sigil: 1>
89
+ bag1 & bag2
90
+ #=> #<Bag phosphor: 1, glyph: 1>
91
+ bag1 - bag2
92
+ #=> #<Bag phosphor: 1, rune: 1>
93
+
94
+ bag1 + {'phosphor' => 1, 'sigil' => 2}
95
+ bag1 + Data.define(:x, :y).new(x: 3, y: 5)
96
+ ```
97
+
98
+ ### Enumeration
99
+
100
+ ```ruby
101
+ bag = Bag.new([1, 2, 2, 3, 3, 3])
102
+
103
+ bag.each { |n| print n }
104
+ #=> 1 2 2 3 3 3
105
+
106
+ bag.each_with_count { |element, count| puts "#{element}: #{count}" }
107
+
108
+ bag.map { |n| n * 2 }
109
+ #=> [2, 4, 4, 6, 6, 6]
110
+ bag.filter { |n| n > 1 }
111
+ #=> [2, 2, 3, 3, 3]
112
+ bag.reduce(:+)
113
+ #=> 14
114
+ ```
115
+
116
+ ### Analysis
117
+
118
+ ```ruby
119
+ bag = Bag.new(%i[ressentiment pathos ethos logos ressentiment ressentiment])
120
+
121
+ bag.sorted_by_count
122
+ #=> [[:ressentiment, 3], [:pathos, 1], ...]
123
+
124
+ bag.sorted_elements
125
+ #=> {ethos: 1, logos: 1, ...}
126
+
127
+ bag.cumulative_counts
128
+ #=> [[:ressentiment, 3], [:pathos, 4], ...]
129
+
130
+ bag.sample
131
+ #=> :ressentiment
132
+ bag.sample(3)
133
+ #=> [:ressentiment, :logos, :ressentiment]
134
+ ```
135
+
136
+ ### Conversion & Comparison
137
+
138
+ ```ruby
139
+ bag = Bag.new(%i[quasar quasar nebula])
140
+
141
+ bag.to_a
142
+ #=> [:quasar, :quasar, :nebula]
143
+ bag.to_h
144
+ #=> {quasar: 2, nebula: 1}
145
+ bag.to_set
146
+ #=> #<Set: {:quasar, :nebula}>
147
+
148
+ Bag.new([1, 2]) < Bag.new([1, 2, 3])
149
+ #=> true
150
+ ```
151
+
152
+ ## Examples
153
+
154
+ ### Word Frequency
155
+
156
+ ```ruby
157
+ text = File.read('beowulf.txt')
158
+ words = Bag.new(text.downcase.scan(/\w+/))
159
+
160
+ words.sorted_by_count.first(10).each do |word, count|
161
+ puts "#{word}: #{count}"
162
+ end
163
+ ```
164
+
165
+ ### Inventory Tracking
166
+
167
+ ```ruby
168
+ inventory = Bag.new
169
+
170
+ inventory.add('widget', 100)
171
+ inventory.remove('widget', 15)
172
+ inventory.count('widget')
173
+ #=> 85
174
+ ```
175
+
176
+ ### Survey Analysis
177
+
178
+ ```ruby
179
+ responses = Bag.new(%w[yes no yes yes maybe yes no])
180
+
181
+ responses.sorted_by_count
182
+ #=> [['yes', 4], ['no', 2], ['maybe', 1]]
183
+
184
+ total = responses.size
185
+ responses.each_with_count do |response, count|
186
+ percentage = (count.fdiv(total * 100)).round(1)
187
+ puts "#{response}: #{percentage}%"
188
+ end
189
+ ```
190
+
191
+ ## Requirements
192
+
193
+ - Ruby 3+
data/Rakefile ADDED
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'minitest/test_task'
5
+ require 'rubocop/rake_task'
6
+
7
+ task default: %i[test rubocop]
8
+
9
+ Minitest::TestTask.create
10
+
11
+ RuboCop::RakeTask.new do |task|
12
+ task.plugins << 'rubocop-minitest'
13
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Bag
4
+ VERSION = '0.1.0'
5
+ end
data/lib/bag.rb ADDED
@@ -0,0 +1,214 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'bag/version'
4
+
5
+ class Bag
6
+ include Comparable
7
+ include Enumerable
8
+
9
+ def initialize(elements = nil)
10
+ @contents = Hash.new(0)
11
+ return unless elements
12
+
13
+ if elements.is_a?(Hash)
14
+ elements.each { |element, count| add(element, count) }
15
+ else
16
+ elements.each { |element| add element }
17
+ end
18
+ end
19
+
20
+ def add(element, occurrences = 1)
21
+ raise ArgumentError, 'bag elements cannot be nil' if element.nil?
22
+ return self if occurrences.zero?
23
+
24
+ @contents[element] += occurrences
25
+ @contents.delete(element) unless @contents[element].positive?
26
+ self
27
+ end
28
+
29
+ def <<(element)
30
+ raise ArgumentError, 'bag elements cannot be nil' if element.nil?
31
+
32
+ @contents[element] += 1
33
+ self
34
+ end
35
+
36
+ def remove(element, occurrences = 1)
37
+ add(element, -occurrences)
38
+ end
39
+
40
+ def remove_all(element)
41
+ @contents.delete(element)
42
+ self
43
+ end
44
+
45
+ def occurrences_of(element) = @contents[element]
46
+ alias count occurrences_of
47
+
48
+ def include?(element) = @contents.key?(element) && @contents[element].positive?
49
+
50
+ def size = @contents.values.sum
51
+ alias length size
52
+
53
+ def uniq_size = @contents.size
54
+
55
+ def each
56
+ return enum_for(__method__) unless block_given?
57
+
58
+ @contents.each { |element, count| count.times { yield element } }
59
+ self
60
+ end
61
+
62
+ def each_with_count(&)
63
+ return enum_for(__method__) unless block_given?
64
+
65
+ @contents.each(&)
66
+ self
67
+ end
68
+
69
+ def to_a = @contents.flat_map { |element, count| [element] * count }
70
+
71
+ def to_h = @contents.dup
72
+
73
+ def keys = @contents.keys
74
+
75
+ def values = @contents.values
76
+
77
+ def sorted_by_count
78
+ @contents.sort_by { |_element, count| -count }.map { |element, count| [element, count] }
79
+ end
80
+
81
+ def sorted_elements
82
+ @contents.sort.to_h
83
+ end
84
+
85
+ def cumulative_counts
86
+ cumulative = 0
87
+ sorted_by_count.map do |element, count|
88
+ cumulative += count
89
+ [element, cumulative]
90
+ end
91
+ end
92
+
93
+ def to_set
94
+ @contents.keys.to_set
95
+ end
96
+
97
+ def empty? = @contents.empty?
98
+
99
+ def clear
100
+ @contents.clear
101
+ self
102
+ end
103
+
104
+ def sample(num = nil, random: Random)
105
+ return (num ? [] : nil) if empty?
106
+
107
+ if num
108
+ raise ArgumentError, 'negative array size' if num.negative?
109
+
110
+ return Array.new(num) { sample(random:) }
111
+ end
112
+
113
+ sample_single(random)
114
+ end
115
+
116
+ def +(other)
117
+ other = from_hash(other)
118
+ result = Bag.new
119
+
120
+ @contents.each { |element, count| result.contents[element] = count }
121
+ other.contents.each { |element, count| result.contents[element] += count }
122
+
123
+ result
124
+ end
125
+
126
+ def &(other)
127
+ other = from_hash(other)
128
+ result = Bag.new
129
+
130
+ @contents.each do |element, count|
131
+ other_count = other.contents[element]
132
+ result.contents[element] = [count, other_count].min if other_count.positive?
133
+ end
134
+
135
+ result
136
+ end
137
+
138
+ def -(other)
139
+ other = from_hash(other)
140
+ result = Bag.new
141
+
142
+ @contents.each do |element, count|
143
+ remaining = count - other.contents[element]
144
+ result.contents[element] = remaining if remaining.positive?
145
+ end
146
+
147
+ result
148
+ end
149
+
150
+ def ==(other)
151
+ return false unless other.is_a?(Bag)
152
+
153
+ @contents == other.contents
154
+ end
155
+
156
+ def <=>(other)
157
+ return nil unless other.is_a?(Bag)
158
+ return size <=> other.size unless size == other.size
159
+ return uniq_size <=> other.uniq_size unless uniq_size == other.uniq_size
160
+
161
+ compare_contents(other)
162
+ end
163
+
164
+ def inspect = "#<Bag: #{@contents.inspect}>"
165
+ alias to_s inspect
166
+
167
+ def pretty_print(pp)
168
+ pp.object_group(self) do
169
+ pp.seplist(@contents, -> { pp.text ',' }) do |element, count|
170
+ pp.breakable
171
+ if element.is_a?(Symbol)
172
+ pp.text element.inspect[1..]
173
+ pp.text ': '
174
+ else
175
+ pp.pp element
176
+ pp.text ' => '
177
+ end
178
+ pp.pp count
179
+ end
180
+ end
181
+ end
182
+
183
+ protected
184
+
185
+ attr_reader :contents
186
+
187
+ private
188
+
189
+ def sample_single(random)
190
+ target_index = random.rand(size)
191
+ current_sum = 0
192
+
193
+ element, = @contents.find do |_, count|
194
+ current_sum += count
195
+ target_index < current_sum
196
+ end
197
+ element
198
+ end
199
+
200
+ def compare_contents(other)
201
+ to_a.sort <=> other.to_a.sort
202
+ rescue ArgumentError
203
+ nil
204
+ end
205
+
206
+ def from_hash(other)
207
+ return other if other.is_a?(Bag)
208
+ raise TypeError, "can't convert #{other.class} to Bag" unless other.respond_to?(:to_h)
209
+
210
+ Bag.new(other.to_h)
211
+ end
212
+
213
+ def coerce(other) = [from_hash(other), self]
214
+ end
data.tar.gz.sig ADDED
@@ -0,0 +1,2 @@
1
+ 0D �
2
+ X���f����0����A��1�-8��j: aVG��Q(X2��3΋���)�#���|�!�H��
metadata ADDED
@@ -0,0 +1,60 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: bag.rb
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Shannon Skipper
8
+ bindir: bin
9
+ cert_chain:
10
+ - |
11
+ -----BEGIN CERTIFICATE-----
12
+ MIIBnjCCAUSgAwIBAgIULmCXzaYrvh/BroPeBvKWunNA4hAwCgYIKoZIzj0EAwIw
13
+ NTEZMBcGA1UEAwwQdGVzdEBleGFtcGxlLmNvbTEYMBYGA1UECgwPR2VtIERldmVs
14
+ b3BtZW50MB4XDTI1MTEwMTE3MTkzN1oXDTI2MTEwMTE3MTkzN1owNTEZMBcGA1UE
15
+ AwwQdGVzdEBleGFtcGxlLmNvbTEYMBYGA1UECgwPR2VtIERldmVsb3BtZW50MFkw
16
+ EwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuYczd7Vw1YnU4rqc1+5PgDB7RKt3n6hR
17
+ gpH3SHvFzBgydoBVEWBN6AZO/japfJKPkFyyoq3czTxpf5Gv9BYpG6MyMDAwHQYD
18
+ VR0OBBYEFCL6dJfmuDOO3qRtOvStGtKZjNIEMA8GA1UdEwEB/wQFMAMBAf8wCgYI
19
+ KoZIzj0EAwIDSAAwRQIge/heFuZqCd6kY0EEpfngKUarj3EezKMtueh9Fj6366sC
20
+ IQC60bGRQkfaXYX8ekTdBtg3VFOFe/8/UCX24qhAZxC3cA==
21
+ -----END CERTIFICATE-----
22
+ date: 1980-01-02 00:00:00.000000000 Z
23
+ dependencies: []
24
+ description: A pure Ruby implementation of a bag (multiset) collection that tracks
25
+ element occurrences
26
+ email:
27
+ - shannonskipper@gmail.com
28
+ executables: []
29
+ extensions: []
30
+ extra_rdoc_files: []
31
+ files:
32
+ - LICENSE.txt
33
+ - README.md
34
+ - Rakefile
35
+ - lib/bag.rb
36
+ - lib/bag/version.rb
37
+ homepage: https://github.com/havenwood/bag.rb
38
+ licenses:
39
+ - MIT
40
+ metadata:
41
+ source_code_uri: https://github.com/havenwood/bag.rb
42
+ rubygems_mfa_required: 'true'
43
+ rdoc_options: []
44
+ require_paths:
45
+ - lib
46
+ required_ruby_version: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: '3.0'
51
+ required_rubygems_version: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ requirements: []
57
+ rubygems_version: 4.0.0.dev
58
+ specification_version: 4
59
+ summary: A bag (multiset) data structure
60
+ test_files: []
metadata.gz.sig ADDED
@@ -0,0 +1 @@
1
+ 0D F붽�% �&��ۄU{�a8��P��Q�,�A Q-�v*�W_�خ�AE�ud��(�5��@<��D