NAME

Sidef::Types::Set::Bag - A multiset (bag) data structure for Sidef

DESCRIPTION

This class implements a multiset (also known as a bag): a collection that allows duplicate elements and tracks the frequency (count) of each unique element. Unlike a regular Sidef::Types::Set::Set, where each element appears at most once, a Bag keeps a count alongside each distinct value, making it useful for frequency analysis and operations that need to preserve multiplicity. It's a subclass of Set.

Internally, a Bag maps each element's serialized key (see "DESCRIPTION" in Sidef::Types::Set::Set for how elements are compared/deduplicated) to a small record holding both the original value and its current count.

SYNOPSIS

var a = Bag(1, 1, 2, 3)
var b = Bag(1, 4, 3, 5)

say(a | b)     #=> Bag(1, 1, 2, 3, 4, 5)
say(a ^ b)     #=> Bag(1, 2, 4, 5)
say(a & b)     #=> Bag(1, 3)
say(a - b)     #=> Bag(1, 2)
say(a + b)     #=> Bag(1, 1, 1, 2, 3, 3, 4, 5)

say(a.count(1)) #=> 2
say(a.elems)    #=> 3
say(a.len)      #=> 4

say(a.keys)     #=> [1, 2, 3]
say(a.freq)     #=> [[1, 2], [2, 1], [3, 1]]

INHERITS

Inherits methods from Sidef::Types::Set::Set.

CONSTRUCTION

new

Bag(*elements)
Bag.new(*elements)

Creates a new bag containing elements, tallying a count for each distinct value. With no arguments, creates an empty bag. Note that this constructor always blesses into the literal Sidef::Types::Set::Bag package itself, regardless of the invocant -- it is not polymorphic.

var bag  = Bag.new
var bag2 = Bag.new(1, 2, 2, 3)   #=> Bag(1, 2, 2, 3)

Aliases: call

SET ALGEBRA

For "union", "intersection", "difference", and "symmetric_difference", the right-hand operand doesn't have to already be a Bag -- if it isn't one, it's first converted with .to_bag. "concat" (the + operator) is the one exception: if other isn't the exact same class, it's added as a single new element (incrementing its own count by 1) rather than merged.

concat

self.concat(other)
self + other

If other is also a Bag (of the exact same class), returns a new bag where every shared element's count is the sum of its counts in both bags (and every element only present in one side keeps its original count). If other is anything else, it is added as a single new element instead.

say(Bag(1, 2) + Bag(2, 3))     #=> Bag(1, 2, 2, 3)

Aliases: operator +

union

self.union(other)
self | other

Returns a new bag where each element's count is the maximum of its counts in self and other.

say(Bag(1, 1, 2) | Bag(1, 3, 3))   #=> Bag(1, 1, 2, 3, 3)

Aliases: or, operator |, operator

intersection

self.intersection(other)
self & other

Returns a new bag containing only the elements present in both self and other, with each count being the minimum of the two.

say(Bag(1, 1, 2, 3) & Bag(1, 2, 2, 4))   #=> Bag(1, 2)

Aliases: and, operator &, operator

difference

self.difference(other)
self - other

Returns a new bag where each element of self has its count reduced by its count in other; an element is dropped entirely once its count would be zero or negative.

say(Bag(1, 1, 2, 3) - Bag(1, 3))   #=> Bag(1, 2)

Aliases: sub, diff, operator -, operator

symmetric_difference

self.symmetric_difference(other)
self ^ other

Returns a new bag with, for each element, the absolute difference between its counts in self and other (an element present in only one bag keeps its full count there; an element with equal counts on both sides is dropped entirely).

say(Bag(1, 1, 2) ^ Bag(1, 3))   #=> Bag(1, 2, 3)

Aliases: xor, symdiff, operator ^

MEMBERSHIP AND COMPARISON

has

self.has(obj)
self ∋ obj

Returns true if obj is an element of the bag (with a count of at least 1), regardless of its actual count, otherwise false.

say(Bag(1, 2, 3) ∋ 2)   #=> true
say(Bag(1, 2, 3) ∋ 5)   #=> false

Aliases: haskey, has_key, exists, include, includes, contain, contains, operator

self ∌ obj

Returns true if obj is not an element of the bag -- the logical negation of "has".

say(Bag(1, 2, 3) ∌ 5)   #=> true
say(Bag(1, 2, 3) ∌ 2)   #=> false

contains_all

self.contains_all(*objects)

Returns true if the bag contains every one of objects with at least as high a count as that object appears among objects itself. In other words, this is multiplicity-aware: asking for 1 twice requires the bag to contain at least two 1s, not just one.

say(Bag(1, 2, 3, 3).contains_all(1, 3))     #=> true
say(Bag(1, 2, 3).contains_all(1, 4))        #=> false
say(Bag(1, 2, 3).contains_all(1, 1))        #=> false  # only one 1 in the bag

is_subset

self.is_subset(other)
self <= other

Returns true if every element of self also appears in other with a count at least as high (other is converted with .to_bag first if it isn't already a bag).

say(Bag(1, 2) <= Bag(1, 2, 2, 3))   #=> true

Aliases: operator <=, operator , operator

is_superset

self.is_superset(other)
self >= other

Returns true if every element of other also appears in self with a count at least as high (other is converted with .to_bag first if it isn't already a bag).

say(Bag(1, 2, 2, 3) >= Bag(1, 2))   #=> true

Aliases: operator >=, operator , operator

eq

self.eq(other)
self ≡ other

Returns true if self and other contain exactly the same elements with exactly the same counts. Handles circular references safely.

say(Bag(1, 2, 2) ≡ Bag(2, 1, 2))   #=> true

Aliases: operator , operator ==

ne

self.ne(other)
self ≠ other

Returns the logical negation of "eq".

say(Bag(1, 2) ≠ Bag(1, 2, 2))   #=> true

Aliases: operator , operator !=

ADDING ELEMENTS

append

self.append(*objects)
self << obj

Adds every one of objects to the bag, incrementing each one's count by 1, and returns self.

var bag = Bag(1, 2)
bag << 2
say(bag)             #=> Bag(1, 2, 2)

Aliases: add, push, operator <<

replace_pair

self.replace_pair(obj, n)

Sets obj's count to exactly n (overwriting whatever it was before, or creating it if it wasn't present), and returns self. Unlike most other mutating methods here, this does not treat a non-positive n specially -- setting a count to 0 or less still leaves obj as a present key (so "has" and "elems" still count it), just contributing nothing (or a negative amount) if the bag is later expanded or summed.

var bag = Bag(1, 2, 2)
bag.replace_pair(2, 5)
say(bag)             #=> Bag(1, 2, 2, 2, 2, 2)

Aliases: set_kv, update_kv, update_pair

replace_pairs

self.replace_pairs(*pairs)

Calls "replace_pair" once for each (obj, n) pair given (flattened, e.g. obj1, n1, obj2, n2, ...), and returns self.

var bag = Bag(1)
bag.replace_pairs(1, 3, 2, 2)
say(bag)             #=> Bag(1, 1, 1, 2, 2)

Aliases: set_kvs, update_kvs, update_pairs

add_pair

self.add_pair(obj, n)

Increments obj's count by n (or sets it to n if obj wasn't already present), and returns self. Like "replace_pair", a non-positive n is not treated specially.

var bag = Bag(1, 2)
bag.add_pair(2, 3)
say(bag)             #=> Bag(1, 2, 2, 2, 2)

Aliases: add_kv, push_kv, append_kv, push_pair, append_pair

add_pairs

self.add_pairs(*pairs)

Calls "add_pair" once for each (obj, n) pair given (flattened), and returns self.

var bag = Bag(1)
bag.add_pairs(2, 3, 3, 2)
say(bag)             #=> Bag(1, 2, 2, 2, 3, 3)

Aliases: add_kvs, push_kvs, append_kvs, push_pairs, append_pairs

REMOVING ELEMENTS

delete

self.delete(*objects)

Decrements each one of objects by 1 (removing it entirely once its count reaches 0), and returns the list of removed values -- not the bag itself.

var bag = Bag(1, 1, 2, 3)
say(bag.delete(1, 2))    #=> (1, 2)
say(bag)                 #=> Bag(1, 3)

Aliases: remove, discard

delete_all

self.delete_all(*objects)

Removes every one of objects from the bag completely, regardless of count, and returns the removed occurrences (each value repeated according to its former count) -- not the bag itself.

var bag = Bag(1, 1, 2, 3)
say(bag.delete_all(1, 2))   #=> (1, 1, 2)
say(bag)                    #=> Bag(3)

Aliases: remove_all, discard_all

delete_key

self.delete_key(obj)

Removes obj from the bag completely, regardless of count, and returns the value that was removed (or nil if it wasn't present) -- not the bag itself.

var bag = Bag(1, 1, 2, 3)
say(bag.delete_key(1))   #=> 1
say(bag)                 #=> Bag(2, 3)

Aliases: remove_key, discard_key

pop

self.pop

Removes and returns an arbitrary element from the bag, decrementing its count by 1 (removing it entirely if its count was 1). Returns nil if the bag is empty.

var bag = Bag(1, 2, 2)
say(bag.pop)         # some element of the bag

shift

self.shift

Removes and returns an arbitrary element from the bag, decrementing its count by 1 (removing it entirely if its count was 1). Returns nil if the bag is empty. As with "shift" in Sidef::Types::Set::Set, since a bag's keys have no defined order, this behaves identically to "pop".

var bag = Bag(1, 2, 2)
say(bag.shift)       # some element of the bag

delete_if

self.delete_if(block)

Removes every element (with all of its occurrences) for which block returns a truthy value, and returns self.

var bag = Bag(1, 2, 2, 3, 4)
bag.delete_if {|n| n.is_even }
say(bag)             #=> Bag(1, 3)

delete_first_if

self.delete_first_if(block)

Finds the first element (in the bag's own unordered iteration order) for which block returns a truthy value, and removes just one occurrence of it (decrementing its count by 1, or removing it entirely if its count was 1). Returns self.

var bag = Bag(1, 2, 3, 4)
bag.delete_first_if {|n| n > 2 }
say(bag)             #=> one of 3 or 4 removed, e.g. Bag(1, 2, 4)

ITERATION

each

self.each(block)

Calls block once for every occurrence of every element (respecting multiplicity), and returns self.

Bag(1, 2, 2).each {|n| say(n) }
# Prints: 1, 2, 2 (in some order)

each_2d

self.each_2d(block)

Like "each_2d" in Sidef::Types::Set::Set: iterates over a bag whose elements are themselves array-like, spreading each element's own contents as separate arguments to block, once per occurrence (respecting multiplicity). Returns self.

var pairs = Bag([1, "a"], [1, "a"], [2, "b"])
pairs.each_2d {|k, v| say("#{k}: #{v}") }
# Prints "1: a" twice and "2: b" once, in some order

each_kv

self.each_kv(block)

Calls block(value, count) once per distinct element (not once per occurrence), passing the element and its total count together. Returns self.

Bag(1, 2, 2, 3).each_kv {|k, v| say("#{k} appears #{v} times") }

iter

self.iter

Returns an iterator (a Block) over the bag's distinct elements (not its individual occurrences): each call to .run returns a 2-element Array of [value, count], or nil once exhausted.

var it = Bag(1, 2, 2).iter
say(it.run)          #=> [1, 1]  (in some order)
say(it.run)          #=> [2, 2]

TRANSFORMATION AND FILTERING

map

self.map(block)

Returns a new bag where each distinct element is replaced by the result(s) of calling block on it once; the resulting element's count is increased by the original element's count (so if block returns more than one value for a given input, each one of them gets the full original count, not a split of it). If block is omitted, elements are kept as-is.

say(Bag(1, 2, 2).map {|n| n * 2 })   #=> Bag(2, 4, 4)

map_2d

self.map_2d(block)

Like "map", but for a bag whose elements are themselves array-like -- each element's own contents are spread as separate arguments to block.

var pairs = Bag([1, "a"], [2, "b"])
say(pairs.map_2d {|k, v| [k * 2, v] })

map_kv

self.map_kv(block)

Calls block(value, count) once per distinct element, expecting it to return a flattened list of zero or more (new_value, new_count) pairs. Each returned pair sets (not adds to) new_value's count in the result -- if two different original elements happen to produce the same new_value, the later one wins rather than the counts being summed. A pair whose new_count is zero or negative is silently dropped.

say(Bag(1, 2).map_kv {|k, v| [k + 1, v * 2] })   #=> Bag(2, 2, 3, 3)

collect

self.collect(block)

Calls block once per distinct element and repeats each result according to that element's original count, collecting everything into a plain Array -- not a new Bag. Because block only runs once per distinct element, all of that element's repeated copies in the result share the exact same computed value (relevant if block is non-deterministic).

say(Bag(1, 2, 2).collect {|n| n * 2 })   #=> [2, 4, 4]  (an Array, order may vary)

grep

self.grep(block)

Returns a new bag keeping only the distinct elements for which block returns a truthy value, with their counts unchanged. If block is omitted, elements are kept based on their own truthiness.

say(Bag(1, 2, 2, 3, 4).grep {|n| n.is_even })   #=> Bag(2, 2, 4)

Aliases: select

grep_2d

self.grep_2d(block)

Like "grep", but for a bag whose elements are themselves array-like -- each element's own contents are spread as separate arguments to block.

var pairs = Bag([10, "a"], [20, "b"], [5, "c"])
say(pairs.grep_2d {|n, label| n > 8 })   #=> Bag([10, "a"], [20, "b"])

grep_kv

self.grep_kv(block)

Like "grep", but calls block(value, count) for each distinct element, passing its count alongside it.

say(Bag(1, 1, 2, 2, 2, 3).grep_kv {|k, v| v >= k })   #=> Bag(1, 1, 2, 2, 2)

AGGREGATION AND PREDICATES

count

self.count(obj)
self.count(block)

If given a plain value, returns obj's current count (0 if not present) -- unlike "count" in Sidef::Types::Set::Set, this is a true frequency, not just membership. If given a block, delegates entirely to "count_by".

say(Bag(1, 1, 2, 3).count(1))   #=> 2
say(Bag(1, 1, 2, 3).count(5))   #=> 0

Aliases: get

count_by

self.count_by(block)

Returns the total count (summed across matching distinct elements, respecting each one's own multiplicity) of every element for which block returns a truthy value. This is a single Number, not a grouped tally.

say(Bag(1, 2, 3, 4).count_by {|n| n.is_even })   #=> 2

min

self.min

Returns the minimum element in the bag, ignoring counts entirely (equivalent to looking only at the distinct keys).

say(Bag(5, 1, 3, 3).min)   #=> 1

min_by

self.min_by(block)

Returns the distinct element for which block returns the smallest value, ignoring counts.

say(Bag(1, 2, 3).min_by {|n| -n })   #=> 3

max

self.max

Returns the maximum element in the bag, ignoring counts entirely.

say(Bag(1, 5, 3, 3).max)   #=> 5

max_by

self.max_by(block)

Returns the distinct element for which block returns the largest value, ignoring counts.

say(Bag(1, 2, 3).max_by {|n| -n })   #=> 1

FREQUENCY INSPECTION

freq

self.freq

Returns an Array of plain [value, count] 2-element arrays, one per distinct element. (Compare with "pairs", which returns the same information as proper Pair objects instead.)

say(Bag(1, 1, 2, 3).freq)   #=> [[1, 2], [2, 1], [3, 1]]

most_common

self.most_common(n)

Returns an Array of the n distinct elements with the highest counts, sorted by count in descending order, as [value, count] pairs.

say(Bag(1, 2, 2, 3, 3, 3).most_common(2))   #=> [[3, 3], [2, 2]]

Aliases: top

uniq

self.uniq

Returns a new bag with every element's count reset to exactly 1 (i.e. behaving like the underlying set, but remaining a Bag).

say(Bag(1, 1, 2, 3, 3).uniq)   #=> Bag(1, 2, 3)

Aliases: unique

elems

self.elems

Returns the number of distinct elements in the bag (ignoring their counts).

say(Bag(1, 1, 2, 3).elems)   #=> 3

Aliases: keys_len

length

self.length

Returns the total number of elements in the bag, counting multiplicities (the sum of every element's count).

say(Bag(1, 1, 2, 3).length)   #=> 4

Aliases: len, size

SORTING AND CONVERSION

sort

self.sort(block)

Expands the bag (respecting counts, via "to_a") and returns the result sorted -- in natural order, or by block if given.

say(Bag(3, 1, 2, 2).sort)                    #=> [1, 2, 2, 3]
say(Bag(3, 1, 2).sort {|a, b| b <=> a })      #=> [3, 2, 1]

sort_by

self.sort_by(block)

Expands the bag (respecting counts) and returns the result sorted by the value block returns for each element.

say(Bag(1, 2, 3, 3).sort_by {|n| -n })   #=> [3, 3, 2, 1]

to_a

self.to_a

Returns an Array with every element of the bag, each one repeated according to its count.

say(Bag(1, 2, 2, 3).to_a)   #=> [1, 2, 2, 3]

Aliases: expand, to_array

to_list

self...

Returns every element of the bag as a plain list (not wrapped in an Array), each one repeated according to its count -- the list-returning counterpart to "to_a".

var bag = Bag(1, 2, 2)
var list = bag...
say([list])          #=> [1, 2, 2]  (order may vary)

Aliases: operator ...

keys

self.keys

Returns an Array of the bag's distinct elements, without their counts.

say(Bag(1, 1, 2, 3).keys)   #=> [1, 2, 3]

values

self.values

Returns an Array of the counts for each distinct element (in the same order as "keys", so keys[i] and values[i] correspond to each other).

say(Bag(1, 1, 2, 3).values)   #=> [2, 1, 1]

pairs

self.pairs

Returns an Array of Pair objects, one per distinct element, pairing each value with its count. (Compare with "freq", which returns the same information as plain 2-element arrays instead.)

say(Bag(1, 1, 2).pairs)   #=> [Pair(1, 2), Pair(2, 1)]

Aliases: kv

to_set

self.to_set

Converts the bag to a plain Set, discarding all count information and keeping only the distinct elements.

say(Bag(1, 1, 2, 3).to_set)   #=> Set(1, 2, 3)

to_bag

self.to_bag

Returns self unchanged (a Bag is already a bag).

var bag = Bag(1, 2, 3)
say(bag.to_bag == bag)   #=> true

clone

self.clone

Returns a shallow copy of the bag: a new bag with the same distinct elements and counts. If an element's own value is itself a reference (e.g. an Array), that nested reference is shared between the original and the clone, not copied.

var a = Bag(1, 2, 3)
var b = a.clone
b << 4
say(a)   #=> Bag(1, 2, 3)
say(b)   #=> Bag(1, 2, 3, 4)

join

self.join(sep)

Expands the bag (respecting counts, via "to_a") and joins the elements into a String, separated by sep.

say(Bag(1, 2, 2, 3).join(", "))   #=> "1, 2, 2, 3"

INSPECTION

dump

self.dump

Returns a String representation of the bag, in the format Bag(element, element, ...), with each element repeated according to its count. Each element is rendered by calling its own dump method if it has one (falling back to plain stringification, or nil, otherwise), and a circular reference back to the bag itself is shown as an abbreviated placeholder rather than expanded infinitely.

say(Bag(1, 2, 2).dump)   #=> "Bag(1, 2, 2)"

get_value

self.get_value

Returns the bag's contents as a plain Perl array reference, with every nested Sidef object recursively unwrapped to its own raw underlying value (via its own get_value) and repeated according to its count. Circular references are handled by reusing the same array reference for a bag already seen during the recursion. This is a low-level method, mainly useful for interop rather than everyday Sidef code.

EXAMPLES

Word frequency counter

var text = "the quick brown fox jumps over the lazy dog the fox runs"
var bag = Bag(text.split(' ')...)

say(bag.most_common(2))     #=> [["the", 3], ["fox", 2]]
say(bag.count("the"))       #=> 3
say(bag.elems)              #=> 9   # distinct words
say(bag.len)                #=> 12  # total words

Building up counts incrementally

var votes = Bag()
votes.add_pair("cats", 5)
votes.add_pair("dogs", 3)
votes.add_pair("cats", 2)     # increments, doesn't overwrite

say(votes.freq)              #=> [["cats", 7], ["dogs", 3]]
say(votes.most_common(1))    #=> [["cats", 7]]

Multiset algebra for inventory tracking

var have = Bag("apple", "apple", "banana", "banana", "banana")
var used = Bag("apple", "banana")

var remaining = have - used
say(remaining)                #=> Bag(apple, banana, banana)

var restocked = remaining | Bag("apple", "apple")
say(restocked)                #=> Bag(apple, apple, banana, banana)

SEE ALSO

Sidef::Types::Set::Set