Skip to content

Commit 7909b30

Browse files
authored
gh-138682: Add symmetric difference to Counter (gh-138766)
1 parent 89b5571 commit 7909b30

File tree

5 files changed

+106
-3
lines changed

5 files changed

+106
-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:: next
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: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,14 @@ New modules
294294
Improved modules
295295
================
296296

297+
collections
298+
-----------
299+
300+
* Added :meth:`!collections.Counter.__xor__` and
301+
:meth:`!collections.Counter.__ixor__` to compute the symmetric difference
302+
between :class:`~collections.Counter` objects.
303+
(Contributed by Raymond Hettinger in :gh:`138682`.)
304+
297305
collections.abc
298306
---------------
299307

Lib/collections/__init__.py

Lines changed: 44 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,33 @@ 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 and count:
936+
result[elem] = abs(count)
937+
return result
938+
911939
def __pos__(self):
912940
'Adds an empty counter, effectively stripping negative and zero counts'
913941
result = Counter()
@@ -990,6 +1018,22 @@ def __iand__(self, other):
9901018
self[elem] = other_count
9911019
return self._keep_positive()
9921020

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

9941038
########################################################################
9951039
### ChainMap

Lib/test/test_collections.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2179,6 +2179,7 @@ def correctly_ordered(seq):
21792179
self.assertTrue(correctly_ordered(p - q))
21802180
self.assertTrue(correctly_ordered(p | q))
21812181
self.assertTrue(correctly_ordered(p & q))
2182+
self.assertTrue(correctly_ordered(p ^ q))
21822183

21832184
p, q = Counter(ps), Counter(qs)
21842185
p += q
@@ -2196,6 +2197,10 @@ def correctly_ordered(seq):
21962197
p &= q
21972198
self.assertTrue(correctly_ordered(p))
21982199

2200+
p, q = Counter(ps), Counter(qs)
2201+
p ^= q
2202+
self.assertTrue(correctly_ordered(p))
2203+
21992204
p, q = Counter(ps), Counter(qs)
22002205
p.update(q)
22012206
self.assertTrue(correctly_ordered(p))
@@ -2278,6 +2283,7 @@ def test_multiset_operations(self):
22782283
(Counter.__sub__, lambda x, y: max(0, x-y)),
22792284
(Counter.__or__, lambda x, y: max(0,x,y)),
22802285
(Counter.__and__, lambda x, y: max(0, min(x,y))),
2286+
(Counter.__xor__, lambda x, y: max(0, max(x,y) - min(x,y))),
22812287
]:
22822288
result = counterop(p, q)
22832289
for x in elements:
@@ -2295,6 +2301,7 @@ def test_multiset_operations(self):
22952301
(Counter.__sub__, set.__sub__),
22962302
(Counter.__or__, set.__or__),
22972303
(Counter.__and__, set.__and__),
2304+
(Counter.__xor__, set.__xor__),
22982305
]:
22992306
counter_result = counterop(p, q)
23002307
set_result = setop(set(p.elements()), set(q.elements()))
@@ -2313,6 +2320,7 @@ def test_inplace_operations(self):
23132320
(Counter.__isub__, Counter.__sub__),
23142321
(Counter.__ior__, Counter.__or__),
23152322
(Counter.__iand__, Counter.__and__),
2323+
(Counter.__ixor__, Counter.__xor__),
23162324
]:
23172325
c = p.copy()
23182326
c_id = id(c)
@@ -2388,6 +2396,7 @@ def test_multiset_operations_equivalent_to_set_operations(self):
23882396
self.assertEqual(set(cp - cq), sp - sq)
23892397
self.assertEqual(set(cp | cq), sp | sq)
23902398
self.assertEqual(set(cp & cq), sp & sq)
2399+
self.assertEqual(set(cp ^ cq), sp ^ sq)
23912400
self.assertEqual(cp == cq, sp == sq)
23922401
self.assertEqual(cp != cq, sp != sq)
23932402
self.assertEqual(cp <= cq, sp <= sq)
@@ -2415,6 +2424,40 @@ def test_gt(self):
24152424
self.assertTrue(Counter(a=3, b=2, c=0) > Counter('aab'))
24162425
self.assertFalse(Counter(a=2, b=1, c=0) > Counter('aab'))
24172426

2427+
def test_symmetric_difference(self):
2428+
population = (-4, -3, -2, -1, 0, 1, 2, 3, 4)
2429+
2430+
for a, b1, b2, c in product(population, repeat=4):
2431+
p = Counter(a=a, b=b1)
2432+
q = Counter(b=b2, c=c)
2433+
r = p ^ q
2434+
2435+
# Elementwise invariants
2436+
for k in ('a', 'b', 'c'):
2437+
self.assertEqual(r[k], max(p[k], q[k]) - min(p[k], q[k]))
2438+
self.assertEqual(r[k], abs(p[k] - q[k]))
2439+
2440+
# Invariant for all positive, negative, and zero counts
2441+
self.assertEqual(r, (p - q) | (q - p))
2442+
2443+
# Invariant for non-negative counts
2444+
if a >= 0 and b1 >= 0 and b2 >= 0 and c >= 0:
2445+
self.assertEqual(r, (p | q) - (p & q))
2446+
2447+
# Zeros and negatives eliminated
2448+
self.assertTrue(all(value > 0 for value in r.values()))
2449+
2450+
# Output preserves input order: p first and then q
2451+
keys = list(p) + list(q)
2452+
indices = [keys.index(k) for k in r]
2453+
self.assertEqual(indices, sorted(indices))
2454+
2455+
# Inplace operation matches binary operation
2456+
pp = Counter(p)
2457+
qq = Counter(q)
2458+
pp ^= qq
2459+
self.assertEqual(pp, r)
2460+
24182461

24192462
def load_tests(loader, tests, pattern):
24202463
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 :class:`collections.Counter` objects.

0 commit comments

Comments
 (0)