objects to produce multisets (counters that have counts greater than zero).
Addition and subtraction combine counters by adding or subtracting the counts
of corresponding elements. Intersection and union return the minimum and
-maximum of corresponding counts. Equality and inclusion compare
-corresponding counts. Each operation can accept inputs with signed
-counts, but the output will exclude results with counts of zero or less.
+maximum of corresponding counts. Symmetric difference returns the difference
+between the maximum and minimum of the corresponding counts. Equality and
+inclusion compare corresponding counts. Each operation can accept inputs
+with signed counts, but the output will exclude results with counts of zero
+or below.
.. doctest::
Counter({'a': 1, 'b': 1})
>>> c | d # union: max(c[x], d[x])
Counter({'a': 3, 'b': 2})
+ >>> c ^ d # max(c[x], d[x]) - min(c[x], d[x])
+ Counter({'a': 2, 'b': 1})
>>> c == d # equality: c[x] == d[x]
False
>>> c <= d # inclusion: c[x] <= d[x]
.. versionadded:: 3.3
Added support for unary plus, unary minus, and in-place multiset operations.
+.. versionadded:: next
+ Added support for the symmetric difference multiset operation, ``c ^ d``.
+
.. note::
Counters were primarily designed to work with positive integers to represent
# set(cp - cq) == sp - sq
# set(cp | cq) == sp | sq
# set(cp & cq) == sp & sq
+ # set(cp ^ cq) == sp ^ sq
def __eq__(self, other):
'True if all counts agree. Missing counts are treated as zero.'
result[elem] = newcount
return result
+ def __xor__(self, other):
+ '''Symmetric difference. Absolute value of count differences.
+
+ The symmetric difference p ^ q is equivalent to:
+
+ (p - q) | (q - p).
+
+ For each element, symmetric difference gives the same result as:
+
+ max(p[elem], q[elem]) - min(p[elem], q[elem])
+
+ >>> Counter(a=5, b=3, c=2, d=2) ^ Counter(a=1, b=3, c=5, e=1)
+ Counter({'a': 4, 'c': 3, 'd': 2, 'e': 1})
+
+ '''
+ if not isinstance(other, Counter):
+ return NotImplemented
+ result = Counter()
+ for elem, count in self.items():
+ newcount = abs(count - other[elem])
+ if newcount:
+ result[elem] = newcount
+ for elem, count in other.items():
+ if elem not in self and count:
+ result[elem] = abs(count)
+ return result
+
def __pos__(self):
'Adds an empty counter, effectively stripping negative and zero counts'
result = Counter()
self[elem] = other_count
return self._keep_positive()
+ def __ixor__(self, other):
+ '''Inplace symmetric difference. Absolute value of count differences.
+
+ >>> c = Counter(a=5, b=3, c=2, d=2)
+ >>> c ^= Counter(a=1, b=3, c=5, e=1)
+ >>> c
+ Counter({'a': 4, 'c': 3, 'd': 2, 'e': 1})
+
+ '''
+ for elem, count in self.items():
+ self[elem] = abs(count - other[elem])
+ for elem, count in other.items():
+ if elem not in self:
+ self[elem] = abs(count)
+ return self._keep_positive()
+
########################################################################
### ChainMap
self.assertTrue(correctly_ordered(p - q))
self.assertTrue(correctly_ordered(p | q))
self.assertTrue(correctly_ordered(p & q))
+ self.assertTrue(correctly_ordered(p ^ q))
p, q = Counter(ps), Counter(qs)
p += q
p &= q
self.assertTrue(correctly_ordered(p))
+ p, q = Counter(ps), Counter(qs)
+ p ^= q
+ self.assertTrue(correctly_ordered(p))
+
p, q = Counter(ps), Counter(qs)
p.update(q)
self.assertTrue(correctly_ordered(p))
(Counter.__sub__, lambda x, y: max(0, x-y)),
(Counter.__or__, lambda x, y: max(0,x,y)),
(Counter.__and__, lambda x, y: max(0, min(x,y))),
+ (Counter.__xor__, lambda x, y: max(0, max(x,y) - min(x,y))),
]:
result = counterop(p, q)
for x in elements:
(Counter.__sub__, set.__sub__),
(Counter.__or__, set.__or__),
(Counter.__and__, set.__and__),
+ (Counter.__xor__, set.__xor__),
]:
counter_result = counterop(p, q)
set_result = setop(set(p.elements()), set(q.elements()))
(Counter.__isub__, Counter.__sub__),
(Counter.__ior__, Counter.__or__),
(Counter.__iand__, Counter.__and__),
+ (Counter.__ixor__, Counter.__xor__),
]:
c = p.copy()
c_id = id(c)
self.assertEqual(set(cp - cq), sp - sq)
self.assertEqual(set(cp | cq), sp | sq)
self.assertEqual(set(cp & cq), sp & sq)
+ self.assertEqual(set(cp ^ cq), sp ^ sq)
self.assertEqual(cp == cq, sp == sq)
self.assertEqual(cp != cq, sp != sq)
self.assertEqual(cp <= cq, sp <= sq)
self.assertTrue(Counter(a=3, b=2, c=0) > Counter('aab'))
self.assertFalse(Counter(a=2, b=1, c=0) > Counter('aab'))
+ def test_symmetric_difference(self):
+ population = (-4, -3, -2, -1, 0, 1, 2, 3, 4)
+
+ for a, b1, b2, c in product(population, repeat=4):
+ p = Counter(a=a, b=b1)
+ q = Counter(b=b2, c=c)
+ r = p ^ q
+
+ # Elementwise invariants
+ for k in ('a', 'b', 'c'):
+ self.assertEqual(r[k], max(p[k], q[k]) - min(p[k], q[k]))
+ self.assertEqual(r[k], abs(p[k] - q[k]))
+
+ # Invariant for all positive, negative, and zero counts
+ self.assertEqual(r, (p - q) | (q - p))
+
+ # Invariant for non-negative counts
+ if a >= 0 and b1 >= 0 and b2 >= 0 and c >= 0:
+ self.assertEqual(r, (p | q) - (p & q))
+
+ # Zeros and negatives eliminated
+ self.assertTrue(all(value > 0 for value in r.values()))
+
+ # Output preserves input order: p first and then q
+ keys = list(p) + list(q)
+ indices = [keys.index(k) for k in r]
+ self.assertEqual(indices, sorted(indices))
+
+ # Inplace operation matches binary operation
+ pp = Counter(p)
+ qq = Counter(q)
+ pp ^= qq
+ self.assertEqual(pp, r)
+
def load_tests(loader, tests, pattern):
tests.addTest(doctest.DocTestSuite(collections))