Skip to content

Commit 22a28b9

Browse files
committed
gh-138682: Add symmetric difference to Counter
1 parent d0c9943 commit 22a28b9

File tree

5 files changed

+109
-3
lines changed

5 files changed

+109
-3
lines changed

Doc/library/collections.rst

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -367,9 +367,11 @@ Several mathematical operations are provided for combining :class:`Counter`
367367
objects to produce multisets (counters that have counts greater than zero).
368368
Addition and subtraction combine counters by adding or subtracting the counts
369369
of corresponding elements. Intersection and union return the minimum and
370-
maximum of corresponding counts. Equality and inclusion compare
371-
corresponding counts. Each operation can accept inputs with signed
372-
counts, but the output will exclude results with counts of zero or less.
370+
maximum of corresponding counts. Symmetric difference returns the difference
371+
between the maximum and minimum of the corresponding counts. Equality and
372+
inclusion compare corresponding counts. Each operation can accept inputs
373+
with signed counts, but the output will exclude results with counts of zero
374+
or below.
373375

374376
.. doctest::
375377

@@ -383,6 +385,8 @@ counts, but the output will exclude results with counts of zero or less.
383385
Counter({'a': 1, 'b': 1})
384386
>>> c | d # union: max(c[x], d[x])
385387
Counter({'a': 3, 'b': 2})
388+
>>> c ^ d # max(c[x], d[x]) - min(c[x], d[x])
389+
Counter({'a': 2, 'b': 1})
386390
>>> c == d # equality: c[x] == d[x]
387391
False
388392
>>> c <= d # inclusion: c[x] <= d[x]
@@ -400,6 +404,9 @@ or subtracting from an empty counter.
400404
.. versionadded:: 3.3
401405
Added support for unary plus, unary minus, and in-place multiset operations.
402406

407+
.. versionadded:: 3.15
408+
Added support for the symmetric difference multiset operation, ``c ^ d``.
409+
403410
.. note::
404411

405412
Counters were primarily designed to work with positive integers to represent

Doc/whatsnew/3.15.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,15 @@ New modules
283283
Improved modules
284284
================
285285

286+
collections
287+
-----------
288+
289+
* Added :meth:`collections.Counter.__xor__` and
290+
:meth:`collections.Counter.__ixor__` to compute the symmetric difference
291+
between Counter objects.
292+
(Contributed by Raymond Hettinger in :gh:`138682`.)
293+
294+
286295
dbm
287296
---
288297

Lib/collections/__init__.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -796,6 +796,7 @@ def __repr__(self):
796796
# set(cp - cq) == sp - sq
797797
# set(cp | cq) == sp | sq
798798
# set(cp & cq) == sp & sq
799+
# set(cp ^ cq) == sp ^ sq
799800

800801
def __eq__(self, other):
801802
'True if all counts agree. Missing counts are treated as zero.'
@@ -908,6 +909,35 @@ def __and__(self, other):
908909
result[elem] = newcount
909910
return result
910911

912+
def __xor__(self, other):
913+
'''Symmetric difference. Absolute value of count differences.
914+
915+
The symmetric difference p ^ q is equivalent to:
916+
917+
(p - q) | (q - p).
918+
919+
For each element, symmetric difference gives the same result as:
920+
921+
max(p[elem], q[elem]) - min(p[elem], q[elem])
922+
923+
>>> Counter(a=5, b=3, c=2, d=2) ^ Counter(a=1, b=3, c=5, e=1)
924+
Counter({'a': 4, 'c': 3, 'd': 2, 'e': 1})
925+
926+
'''
927+
if not isinstance(other, Counter):
928+
return NotImplemented
929+
result = Counter()
930+
for elem, count in self.items():
931+
newcount = abs(count - other[elem])
932+
if newcount:
933+
result[elem] = newcount
934+
for elem, count in other.items():
935+
if elem not in self:
936+
newcount = abs(count)
937+
if newcount:
938+
result[elem] = newcount
939+
return result
940+
911941
def __pos__(self):
912942
'Adds an empty counter, effectively stripping negative and zero counts'
913943
result = Counter()
@@ -990,6 +1020,22 @@ def __iand__(self, other):
9901020
self[elem] = other_count
9911021
return self._keep_positive()
9921022

1023+
def __ixor__(self, other):
1024+
'''Inplace symmetric difference. Absolute value of count differences.
1025+
1026+
>>> c = Counter(a=5, b=3, c=2, d=2)
1027+
>>> c ^= Counter(a=1, b=3, c=5, e=1)
1028+
>>> c
1029+
Counter({'a': 4, 'c': 3, 'd': 2, 'e': 1})
1030+
1031+
'''
1032+
for elem, count in self.items():
1033+
self[elem] = abs(count - other[elem])
1034+
for elem, count in other.items():
1035+
if elem not in self:
1036+
self[elem] = abs(count)
1037+
return self._keep_positive()
1038+
9931039

9941040
########################################################################
9951041
### ChainMap

Lib/test/test_collections.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2140,6 +2140,7 @@ def correctly_ordered(seq):
21402140
self.assertTrue(correctly_ordered(p - q))
21412141
self.assertTrue(correctly_ordered(p | q))
21422142
self.assertTrue(correctly_ordered(p & q))
2143+
self.assertTrue(correctly_ordered(p ^ q))
21432144

21442145
p, q = Counter(ps), Counter(qs)
21452146
p += q
@@ -2157,6 +2158,10 @@ def correctly_ordered(seq):
21572158
p &= q
21582159
self.assertTrue(correctly_ordered(p))
21592160

2161+
p, q = Counter(ps), Counter(qs)
2162+
p ^= q
2163+
self.assertTrue(correctly_ordered(p))
2164+
21602165
p, q = Counter(ps), Counter(qs)
21612166
p.update(q)
21622167
self.assertTrue(correctly_ordered(p))
@@ -2239,6 +2244,7 @@ def test_multiset_operations(self):
22392244
(Counter.__sub__, lambda x, y: max(0, x-y)),
22402245
(Counter.__or__, lambda x, y: max(0,x,y)),
22412246
(Counter.__and__, lambda x, y: max(0, min(x,y))),
2247+
(Counter.__xor__, lambda x, y: max(0, max(x,y) - min(x,y))),
22422248
]:
22432249
result = counterop(p, q)
22442250
for x in elements:
@@ -2256,6 +2262,7 @@ def test_multiset_operations(self):
22562262
(Counter.__sub__, set.__sub__),
22572263
(Counter.__or__, set.__or__),
22582264
(Counter.__and__, set.__and__),
2265+
(Counter.__xor__, set.__xor__),
22592266
]:
22602267
counter_result = counterop(p, q)
22612268
set_result = setop(set(p.elements()), set(q.elements()))
@@ -2274,6 +2281,7 @@ def test_inplace_operations(self):
22742281
(Counter.__isub__, Counter.__sub__),
22752282
(Counter.__ior__, Counter.__or__),
22762283
(Counter.__iand__, Counter.__and__),
2284+
(Counter.__ixor__, Counter.__xor__),
22772285
]:
22782286
c = p.copy()
22792287
c_id = id(c)
@@ -2349,6 +2357,7 @@ def test_multiset_operations_equivalent_to_set_operations(self):
23492357
self.assertEqual(set(cp - cq), sp - sq)
23502358
self.assertEqual(set(cp | cq), sp | sq)
23512359
self.assertEqual(set(cp & cq), sp & sq)
2360+
self.assertEqual(set(cp ^ cq), sp ^ sq)
23522361
self.assertEqual(cp == cq, sp == sq)
23532362
self.assertEqual(cp != cq, sp != sq)
23542363
self.assertEqual(cp <= cq, sp <= sq)
@@ -2376,6 +2385,40 @@ def test_gt(self):
23762385
self.assertTrue(Counter(a=3, b=2, c=0) > Counter('aab'))
23772386
self.assertFalse(Counter(a=2, b=1, c=0) > Counter('aab'))
23782387

2388+
def test_symmetric_difference(self):
2389+
pop = (-4, -3, -2, -1, 0, 1, 2, 3, 4)
2390+
2391+
for a, b1, b2, c in product(pop, repeat=4):
2392+
p = Counter(a=a, b=b1)
2393+
q = Counter(b=b2, c=c)
2394+
r = p ^ q
2395+
2396+
# Elementwise invariants
2397+
for k in ('a', 'b', 'c'):
2398+
self.assertEqual(r[k], max(p[k], q[k]) - min(p[k], q[k]))
2399+
self.assertEqual(r[k], abs(p[k] - q[k]))
2400+
2401+
# Invariant for all positive, negative, and zero counts
2402+
self.assertEqual(r, (p - q) | (q - p))
2403+
2404+
# Invariant for non-negative counts
2405+
if a >= 0 and b1 >= 0 and b2 >= 0 and c >= 0:
2406+
self.assertEqual(r, (p | q) - (p & q))
2407+
2408+
# Zeros and negatives eliminated
2409+
self.assertTrue(all(value > 0 for value in r.values()))
2410+
2411+
# Output preserves input order: p first and then q
2412+
keys = list(p) + list(q)
2413+
indices = [keys.index(k) for k in r]
2414+
self.assertEqual(indices, sorted(indices))
2415+
2416+
# Inplace operation matches binary operation
2417+
pp = Counter(p)
2418+
qq = Counter(q)
2419+
pp ^= qq
2420+
assert pp == r
2421+
23792422

23802423
def load_tests(loader, tests, pattern):
23812424
tests.addTest(doctest.DocTestSuite(collections))
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added symmetric difference support to Counter objects.

0 commit comments

Comments
 (0)