bitarray 3.12.2__cp313-cp313-pyemscripten_2025_0_wasm32.whl

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.
bitarray/util.py ADDED
@@ -0,0 +1,694 @@
1
+ # Copyright (c) 2019 - 2026, Ilan Schnell; All Rights Reserved
2
+ # bitarray is published under the PSF license.
3
+ #
4
+ # Author: Ilan Schnell
5
+ """
6
+ Useful utilities for working with bitarrays.
7
+ """
8
+ import os
9
+ import sys
10
+ import math
11
+ import random
12
+ import operator
13
+ import itertools
14
+
15
+ from bitarray import bitarray, frozenbitarray, bits2bytes
16
+
17
+ from bitarray._util import (
18
+ zeros, ones, count_n, parity, xor_indices,
19
+ count_and, count_or, count_xor, any_and, subset,
20
+ correspond_all, byteswap,
21
+ serialize, deserialize,
22
+ ba2hex, hex2ba,
23
+ ba2base, base2ba,
24
+ sc_encode, sc_decode,
25
+ rl_encode, rl_decode,
26
+ vl_encode, vl_decode,
27
+ canonical_decode,
28
+ )
29
+ import bitarray._util as _util
30
+
31
+ __all__ = [
32
+ 'zeros', 'ones', 'urandom', 'random_k', 'random_p', 'gen_primes',
33
+ 'pprint', 'strip', 'count_n',
34
+ 'parity', 'sum_indices', 'xor_indices',
35
+ 'count_and', 'count_or', 'count_xor', 'any_and', 'subset',
36
+ 'correspond_all', 'byteswap', 'intervals',
37
+ 'ba2hex', 'hex2ba',
38
+ 'ba2base', 'base2ba',
39
+ 'ba2int', 'int2ba',
40
+ 'serialize', 'deserialize',
41
+ 'sc_encode', 'sc_decode', 'sc_stat',
42
+ 'rl_encode', 'rl_decode',
43
+ 'vl_encode', 'vl_decode',
44
+ 'huffman_code', 'canonical_huffman', 'canonical_decode',
45
+ ]
46
+
47
+
48
+ def urandom(__n, endian=None):
49
+ """urandom(n, /, endian=None) -> bitarray
50
+
51
+ Return random bitarray of length `n` (uses `os.urandom()`).
52
+ """
53
+ a = bitarray(os.urandom(bits2bytes(__n)), endian)
54
+ del a[__n:]
55
+ return a
56
+
57
+
58
+ def random_k(__n, k, endian=None):
59
+ """random_k(n, /, k, endian=None) -> bitarray
60
+
61
+ Return (pseudo-) random bitarray of length `n` with `k` elements
62
+ set to one. Mathematically equivalent to setting (in a bitarray of
63
+ length `n`) all bits at indices `random.sample(range(n), k)` to one.
64
+ The random bitarrays are reproducible when calling Python's `random.seed()`
65
+ with a specific seed value.
66
+ """
67
+ r = _Random(__n, endian)
68
+ k = operator.index(k)
69
+ return r.random_k(k)
70
+
71
+
72
+ def random_p(__n, p=0.5, endian=None):
73
+ """random_p(n, /, p=0.5, endian=None) -> bitarray
74
+
75
+ Return (pseudo-) random bitarray of length `n`, where each bit has
76
+ probability `p` of being one (independent of any other bits). Mathematically
77
+ equivalent to `bitarray((random() < p for _ in range(n)), endian)`, but much
78
+ faster for large `n`. The random bitarrays are reproducible when calling
79
+ Python's `random.seed()` with a specific seed value.
80
+
81
+ This function requires Python 3.12 or higher, as it depends on the standard
82
+ library function `random.binomialvariate()`. Raises `NotImplementedError`
83
+ when Python version is too low.
84
+ """
85
+ if sys.version_info[:2] < (3, 12):
86
+ raise NotImplementedError("bitarray.util.random_p() requires "
87
+ "Python 3.12 or higher")
88
+ r = _Random(__n, endian)
89
+ return r.random_p(p)
90
+
91
+
92
+ class _Random:
93
+
94
+ # To better understand how the algorithm works, see ./doc/random_p.rst
95
+ # See also, Internal_Tests and VerificationTests in devel/test_random.py
96
+
97
+ # maximal number of calls to .random_half() in .combine_half()
98
+ M = 8
99
+
100
+ # number of resulting probability intervals
101
+ K = 1 << M
102
+
103
+ # limit for setting individual bits randomly
104
+ SMALL_P = 0.01
105
+
106
+ def __init__(self, n=0, endian=None):
107
+ self.n = n
108
+ self.nbytes = bits2bytes(n)
109
+ self.endian = endian
110
+
111
+ def random_half(self):
112
+ """
113
+ Return bitarray with each bit having probability p = 1/2 of being 1.
114
+ """
115
+ nbytes = self.nbytes
116
+ # Use random module function for reproducibility (not urandom()).
117
+ # random.randbytes() can be used once Python 3.9 is the minimum
118
+ # supported version.
119
+ b = random.getrandbits(8 * nbytes).to_bytes(nbytes, 'little')
120
+ a = bitarray(b, self.endian)
121
+ del a[self.n:]
122
+ return a
123
+
124
+ def op_seq(self, i):
125
+ """
126
+ Return bitarray containing operator sequence.
127
+ Each item represents a bitwise operation: 0: AND 1: OR
128
+ After applying the sequence (see .combine_half()), we
129
+ obtain a bitarray with probability q = i / K
130
+ """
131
+ if not 0 < i < self.K:
132
+ raise ValueError("0 < i < %d, got i = %d" % (self.K, i))
133
+
134
+ # sequence of &, | operations - least significant operations first
135
+ a = bitarray(i.to_bytes(2, byteorder="little"), "little")
136
+ return a[a.index(1) + 1 : self.M]
137
+
138
+ def combine_half(self, seq):
139
+ """
140
+ Combine random bitarrays with probability 1/2
141
+ according to given operator sequence.
142
+ """
143
+ a = self.random_half()
144
+ for k in seq:
145
+ if k:
146
+ a |= self.random_half()
147
+ else:
148
+ a &= self.random_half()
149
+ return a
150
+
151
+ def adjust_count(self, a, increase):
152
+ """
153
+ Adjust the number of set bits in bitarray `a` by `increase`.
154
+ The length of `a` must equal `self.n`, and `increase` must be in the
155
+ range `-a.count() <= increase <= self.n - a.count()`. The bitarray
156
+ is modified in-place at positions selected uniformly at random.
157
+ """
158
+ randrange = random.randrange
159
+ n = self.n
160
+
161
+ if increase > 0: # not enough 1 bits - increase count
162
+ for _ in range(increase):
163
+ i = randrange(n)
164
+ while a[i]:
165
+ i = randrange(n)
166
+ a[i] = 1
167
+ elif increase < 0: # too many 1 bits - decrease count
168
+ for _ in range(-increase):
169
+ i = randrange(n)
170
+ while not a[i]:
171
+ i = randrange(n)
172
+ a[i] = 0
173
+
174
+ def random_k(self, k):
175
+ n = self.n
176
+ # error check inputs and handle edge cases
177
+ if k <= 0 or k >= n:
178
+ if k == 0:
179
+ return zeros(n, self.endian)
180
+ if k == n:
181
+ return ones(n, self.endian)
182
+ raise ValueError("k must be in range 0 <= k <= n, got %s" % k)
183
+
184
+ # exploit symmetry to establish: k <= n // 2
185
+ if k > n // 2:
186
+ a = self.random_k(n - k)
187
+ a.invert() # use in-place to avoid copying
188
+ return a
189
+
190
+ # decide on sequence, see VerificationTests devel/test_random.py
191
+ if k < 16 or k * self.K < 3 * n:
192
+ i = 0
193
+ else:
194
+ p = k / n # p <= 0.5
195
+ p -= (0.2 - 0.4 * p) / math.sqrt(n)
196
+ i = int(p * (self.K + 1))
197
+
198
+ # combine random bitarrays using bitwise AND and OR operations
199
+ if i < 3:
200
+ a = zeros(n, self.endian)
201
+ self.adjust_count(a, k)
202
+ else:
203
+ a = self.combine_half(self.op_seq(i))
204
+ self.adjust_count(a, k - a.count())
205
+
206
+ return a
207
+
208
+ def random_p(self, p):
209
+ n = self.n
210
+ endian = self.endian
211
+ # error check inputs and handle edge cases
212
+ if p <= 0.0 or p == 0.5 or p >= 1.0 or not math.isfinite(p):
213
+ if p == 0.0:
214
+ return zeros(n, endian)
215
+ if p == 0.5:
216
+ return self.random_half()
217
+ if p == 1.0:
218
+ return ones(n, endian)
219
+ raise ValueError("p must be in range 0.0 <= p <= 1.0, got %s" % p)
220
+
221
+ # for small n, use literal definition
222
+ if n < 16:
223
+ return bitarray((random.random() < p for _ in range(n)), endian)
224
+
225
+ # exploit symmetry to establish: p < 0.5
226
+ if p > 0.5:
227
+ a = self.random_p(1.0 - p)
228
+ a.invert() # use in-place to avoid copying
229
+ return a
230
+
231
+ # for small p, set randomly individual bits
232
+ if p < self.SMALL_P:
233
+ a = zeros(n, endian)
234
+ self.adjust_count(a, random.binomialvariate(n, p))
235
+ return a
236
+
237
+ # calculate operator sequence
238
+ i = int(p * self.K)
239
+ if p * (self.K + 1) > i + 1: # see devel/test_random.py
240
+ i += 1
241
+ seq = self.op_seq(i)
242
+ q = i / self.K
243
+
244
+ # when n is small compared to number of operations, also use literal
245
+ if n < 100 and self.nbytes <= len(seq) + 3 * (q != p):
246
+ return bitarray((random.random() < p for _ in range(n)), endian)
247
+
248
+ # combine random bitarrays using bitwise AND and OR operations
249
+ a = self.combine_half(seq)
250
+ if q < p:
251
+ x = (p - q) / (1.0 - q)
252
+ a |= self.random_p(x)
253
+ elif q > p:
254
+ x = p / q
255
+ a &= self.random_p(x)
256
+
257
+ return a
258
+
259
+
260
+ def gen_primes(__n, endian=None, odd=False):
261
+ """gen_primes(n, /, endian=None, odd=False) -> bitarray
262
+
263
+ Generate a bitarray of length `n` in which active indices are prime numbers.
264
+ By default (`odd=False`), active indices correspond to prime numbers directly.
265
+ When `odd=True`, only odd prime numbers are represented in the resulting
266
+ bitarray `a`, and `a[i]` corresponds to `2*i+1` being prime or not.
267
+ """
268
+ n = operator.index(__n)
269
+ if n < 0:
270
+ raise ValueError("bitarray length must be >= 0")
271
+
272
+ if odd:
273
+ a = ones(105, endian) # 105 = 3 * 5 * 7
274
+ a[1::3] = 0
275
+ a[2::5] = 0
276
+ a[3::7] = 0
277
+ f = "01110110"
278
+ else:
279
+ a = ones(210, endian) # 210 = 2 * 3 * 5 * 7
280
+ for i in 2, 3, 5, 7:
281
+ a[::i] = 0
282
+ f = "00110101"
283
+
284
+ # repeating the array many times is faster than setting the multiples
285
+ # of the low primes to 0
286
+ a *= (n + len(a) - 1) // len(a)
287
+ a[:8] = bitarray(f, endian)
288
+ del a[n:]
289
+ # perform sieve starting at 11
290
+ if odd:
291
+ for i in a.search(1, 5, int(math.sqrt(n // 2) + 1.0)): # 11//2 = 5
292
+ j = 2 * i + 1
293
+ a[(j * j) // 2 :: j] = 0
294
+ else:
295
+ # i*i is always odd, and even bits are already set to 0: use step 2*i
296
+ for i in a.search(1, 11, int(math.sqrt(n) + 1.0)):
297
+ a[i * i :: 2 * i] = 0
298
+ return a
299
+
300
+
301
+ def sum_indices(__a, mode=1):
302
+ """sum_indices(a, /, mode=1) -> int
303
+
304
+ Return sum of indices of all active bits in bitarray `a`.
305
+ Equivalent to `sum(i for i, v in enumerate(a) if v)`.
306
+ `mode=2` sums square of indices.
307
+ """
308
+ # For details see: devel/test_sum_indices.py
309
+ n = 1 << 19 # block size 512 Kbits
310
+ if len(__a) <= n: # shortcut for single block
311
+ return _util.ssqi(__a, mode)
312
+
313
+ if not isinstance(__a, bitarray):
314
+ raise TypeError("bitarray expected, got '%s'" % type(__a).__name__)
315
+ mode = operator.index(mode)
316
+ if mode not in (1, 2):
317
+ raise ValueError("unexpected mode %r" % mode)
318
+
319
+ # Constants
320
+ m = n // 8 # block size in bytes
321
+ o1 = n * (n - 1) // 2
322
+ o2 = o1 * (2 * n - 1) // 3
323
+
324
+ nblocks = (len(__a) + n - 1) // n
325
+ padbits = __a.padbits
326
+ sm = 0
327
+ for i in range(nblocks):
328
+ # use memoryview to avoid copying memory
329
+ v = memoryview(__a)[i * m : (i + 1) * m]
330
+ block = bitarray(None, __a.endian, buffer=v)
331
+ if padbits and i == nblocks - 1:
332
+ if block.readonly:
333
+ block = bitarray(block)
334
+ block[-padbits:] = 0
335
+
336
+ k = block.count()
337
+ if k:
338
+ y = n * i
339
+ z1 = o1 if k == n else _util.ssqi(block)
340
+ if mode == 1:
341
+ sm += k * y + z1
342
+ else:
343
+ z2 = o2 if k == n else _util.ssqi(block, 2)
344
+ sm += (k * y + 2 * z1) * y + z2
345
+
346
+ return sm
347
+
348
+
349
+ def pprint(__a, stream=None, group=8, indent=4, width=80):
350
+ """pprint(bitarray, /, stream=None, group=8, indent=4, width=80)
351
+
352
+ Pretty-print bitarray object to `stream`, defaults to `sys.stdout`.
353
+ By default, bits are grouped in bytes (8 bits), and 64 bits per line.
354
+ Non-bitarray objects are printed using `pprint.pprint()`.
355
+ """
356
+ if stream is None:
357
+ stream = sys.stdout
358
+
359
+ if not isinstance(__a, bitarray):
360
+ import pprint as _pprint
361
+ _pprint.pprint(__a, stream=stream, indent=indent, width=width)
362
+ return
363
+
364
+ group = operator.index(group)
365
+ if group < 1:
366
+ raise ValueError('group must be >= 1')
367
+ indent = operator.index(indent)
368
+ if indent < 0:
369
+ raise ValueError('indent must be >= 0')
370
+ width = operator.index(width)
371
+ if width <= indent:
372
+ raise ValueError('width must be > %d (indent)' % indent)
373
+
374
+ gpl = (width - indent) // (group + 1) # groups per line
375
+ epl = group * gpl # elements per line
376
+ if epl == 0:
377
+ epl = max(1, width - indent - 2)
378
+ type_name = type(__a).__name__
379
+ n = len(__a)
380
+ multiline = n > 0 and ( # here 4 is len("('')")
381
+ len(type_name) + 4 + n + (n - 1) // group >= width
382
+ )
383
+ quotes = "'''" if multiline else ("'" if __a else "")
384
+
385
+ stream.write("%s(%s" % (type_name, quotes))
386
+ for i, b in enumerate(__a):
387
+ if multiline and i % epl == 0:
388
+ stream.write('\n%s' % (indent * ' '))
389
+ if i % group == 0 and i % epl != 0:
390
+ stream.write(' ')
391
+ stream.write(str(b))
392
+
393
+ if multiline:
394
+ stream.write('\n')
395
+
396
+ stream.write("%s)\n" % quotes)
397
+ stream.flush()
398
+
399
+
400
+ def strip(__a, mode='right'):
401
+ """strip(bitarray, /, mode='right') -> bitarray
402
+
403
+ Return a new bitarray with zeros stripped from left, right or both ends.
404
+ Allowed values for mode are the strings: `left`, `right`, `both`
405
+ """
406
+ if not isinstance(mode, str):
407
+ raise TypeError("str expected for mode, got '%s'" %
408
+ type(mode).__name__)
409
+ if mode not in ('left', 'right', 'both'):
410
+ raise ValueError("mode must be 'left', 'right' or 'both', got %r" %
411
+ mode)
412
+
413
+ start = None if mode == 'right' else __a.find(1)
414
+ if start == -1:
415
+ return __a[:0]
416
+ stop = None if mode == 'left' else __a.find(1, right=1) + 1
417
+ return __a[start:stop]
418
+
419
+
420
+ def intervals(__a):
421
+ """intervals(bitarray, /) -> iterator
422
+
423
+ Compute all uninterrupted intervals of 1s and 0s, and return an
424
+ iterator over tuples `(value, start, stop)`. The intervals are guaranteed
425
+ to be in order, and their size is always non-zero (`stop - start > 0`).
426
+ """
427
+ n = len(__a)
428
+ if n == 0:
429
+ return
430
+
431
+ value = __a[0] # value of current interval
432
+ start = 0
433
+ while start < n:
434
+ stop = __a.find(not value, start)
435
+ if stop < 0:
436
+ stop = n
437
+ yield int(value), start, stop
438
+ start = stop
439
+ value = not value # next interval has opposite value
440
+
441
+
442
+ def ba2int(__a, signed=False):
443
+ """ba2int(bitarray, /, signed=False) -> int
444
+
445
+ Convert the given bitarray to an integer.
446
+ The bit-endianness of the bitarray is respected.
447
+ `signed` indicates whether two's complement is used to represent the integer.
448
+ """
449
+ if not isinstance(__a, bitarray):
450
+ raise TypeError("bitarray expected, got '%s'" % type(__a).__name__)
451
+
452
+ length = len(__a)
453
+ if length == 0:
454
+ raise ValueError("non-empty bitarray expected")
455
+
456
+ res = int.from_bytes(__a.tobytes(), byteorder=__a.endian)
457
+
458
+ # Big-endian pad bits are on the right and must be shifted away.
459
+ if __a.endian == 'big':
460
+ res >>= __a.padbits
461
+
462
+ if signed and res & (1 << (length - 1)):
463
+ res -= 1 << length
464
+
465
+ return res
466
+
467
+
468
+ def int2ba(__i, length=None, endian=None, signed=False):
469
+ """int2ba(int, /, length=None, endian=None, signed=False) -> bitarray
470
+
471
+ Convert the given integer to a bitarray (with given bit-endianness,
472
+ and no leading (big-endian) / trailing (little-endian) zeros), unless
473
+ the `length` of the bitarray is provided. An `OverflowError` is raised
474
+ if the integer is not representable with the given number of bits.
475
+ `signed` determines whether two's complement is used to represent the integer,
476
+ and requires `length` to be provided.
477
+ """
478
+ i = operator.index(__i)
479
+ if length is not None:
480
+ length = operator.index(length)
481
+ if length <= 0:
482
+ raise ValueError("length must be > 0")
483
+
484
+ if signed:
485
+ if length is None:
486
+ raise TypeError("signed requires argument 'length'")
487
+ m = 1 << (length - 1)
488
+ if not (-m <= i < m):
489
+ raise OverflowError("signed integer not in range(%d, %d), "
490
+ "got %d" % (-m, m, i))
491
+ if i < 0:
492
+ i += 1 << length
493
+ elif length and i >> length:
494
+ raise OverflowError("unsigned integer not in range(0, %d), "
495
+ "got %d" % (1 << length, i))
496
+
497
+ if length is None:
498
+ length = max(1, i.bit_length())
499
+
500
+ a = bitarray(0, endian)
501
+ a.frombytes(i.to_bytes(bits2bytes(length), byteorder=a.endian))
502
+
503
+ if a.endian == 'little':
504
+ del a[length:]
505
+ else:
506
+ del a[:-length]
507
+ return a
508
+
509
+
510
+ def sc_stat(stream):
511
+ """sc_stat(stream) -> dict
512
+
513
+ Scan a stream produced by `sc_encode()` and return a dictionary containing
514
+ its bit-endianness (`endian`), length (`nbits`), and the number of blocks
515
+ of each type (`blocks`). `blocks` is a list such that `blocks[i]` is the
516
+ number of blocks of type `i`.
517
+
518
+ Like `sc_decode()`, it consumes one encoded bitarray and leaves remaining
519
+ input untouched.
520
+ """
521
+ def decode_header(stream):
522
+ head = next(stream)
523
+ if head & 0xe0:
524
+ raise ValueError("invalid header: 0x%02x" % head)
525
+ endian = 'big' if head & 0x10 else 'little'
526
+ length = head & 0x0f # number of bytes representing nbits
527
+ nbits = 0
528
+ for j in range(length):
529
+ nbits |= next(stream) << 8 * j
530
+ return dict(endian=endian, nbits=nbits)
531
+
532
+ def scan_block(stream, blocks):
533
+ head = next(stream)
534
+
535
+ if head < 0xa0: # type 0 - 0x00 -- 0x9f
536
+ if head == 0: # stop byte
537
+ return False
538
+ n = 0
539
+ k = head if head <= 32 else 32 * (head - 31)
540
+ elif head < 0xc0: # type 1 - 0xa0 .. 0xbf
541
+ n = 1
542
+ k = head - 0xa0
543
+ elif 0xc2 <= head <= 0xc4: # type 2 .. 4 - 0xc2 .. 0xc4
544
+ n = head - 0xc0
545
+ k = next(stream) # index count byte
546
+ else:
547
+ raise ValueError("invalid block head: 0x%02x" % head)
548
+
549
+ blocks[n] += 1
550
+
551
+ nc = max(1, n) * k # size of block data to consume
552
+ next(itertools.islice(stream, nc, nc), None)
553
+ return True
554
+
555
+ stream = iter(stream)
556
+ stats = decode_header(stream)
557
+
558
+ blocks = 5 * [0]
559
+ while scan_block(stream, blocks):
560
+ pass
561
+
562
+ stats['blocks'] = blocks
563
+ return stats
564
+
565
+ # ------------------------------ Huffman coding -----------------------------
566
+
567
+ def _huffman_tree(__freq_map):
568
+ """_huffman_tree(dict, /) -> Node
569
+
570
+ Given a dict mapping symbols to their frequency, construct a Huffman tree
571
+ and return its root node.
572
+ """
573
+ from heapq import heappush, heappop
574
+
575
+ class Node:
576
+ """
577
+ There are two types of Node instances (both have 'freq' attribute):
578
+ * leaf node: has 'symbol' attribute
579
+ * parent node: has 'child' attribute (tuple with both children)
580
+ """
581
+ def __lt__(self, other):
582
+ # heapq needs to be able to compare the nodes
583
+ return self.freq < other.freq
584
+
585
+ minheap = []
586
+ # create all leaf nodes and push them onto the queue
587
+ for sym, f in __freq_map.items():
588
+ leaf = Node()
589
+ leaf.symbol = sym
590
+ leaf.freq = f
591
+ heappush(minheap, leaf)
592
+
593
+ # repeat the process until only one node remains
594
+ while len(minheap) > 1:
595
+ # take the two nodes with lowest frequencies from the queue
596
+ # to construct a new parent node and push it onto the queue
597
+ parent = Node()
598
+ parent.child = heappop(minheap), heappop(minheap)
599
+ parent.freq = parent.child[0].freq + parent.child[1].freq
600
+ heappush(minheap, parent)
601
+
602
+ # the single remaining node is the root of the Huffman tree
603
+ return minheap[0]
604
+
605
+
606
+ def _check_anydict(d):
607
+ types = dict if sys.version_info[:2] < (3, 15) else (dict, frozendict)
608
+ if not isinstance(d, types):
609
+ raise TypeError("dict expected, got '%s'" % type(d).__name__)
610
+ if not d:
611
+ raise ValueError("cannot create Huffman code with no symbols")
612
+
613
+
614
+ def huffman_code(__freq_map, endian=None):
615
+ """huffman_code(dict, /, endian=None) -> dict
616
+
617
+ Given a frequency map, a dictionary mapping symbols to their frequency,
618
+ calculate the Huffman code, i.e. a dict mapping those symbols to
619
+ frozenbitarrays (with given bit-endianness). Note that the symbols are not
620
+ limited to being strings. Symbols may be any hashable object.
621
+ """
622
+ _check_anydict(__freq_map)
623
+
624
+ if len(__freq_map) == 1:
625
+ # With one symbol, Huffman coding would normally assign it an empty
626
+ # code. Here the code must be nonempty, so that .encode() represents
627
+ # each occurrence and .decode() can recover their number. We use 0,
628
+ # leaving 1 unassigned; therefore the prefix code is incomplete.
629
+ sym = next(iter(__freq_map))
630
+ return {sym: frozenbitarray('0', endian)}
631
+
632
+ result = {}
633
+
634
+ def traverse(nd, prefix):
635
+ try: # leaf
636
+ result[nd.symbol] = prefix
637
+ except AttributeError: # parent, so traverse each child
638
+ traverse(nd.child[0], prefix + '0')
639
+ traverse(nd.child[1], prefix + '1')
640
+
641
+ traverse(_huffman_tree(__freq_map), frozenbitarray(0, endian))
642
+ return result
643
+
644
+
645
+ def canonical_huffman(__freq_map):
646
+ """canonical_huffman(dict, /) -> tuple
647
+
648
+ Given a frequency map, a dictionary mapping symbols to their frequency,
649
+ calculate the canonical Huffman code. Returns a tuple containing:
650
+
651
+ 0. the canonical Huffman code as a dict mapping symbols to frozenbitarrays
652
+ 1. a list containing the number of symbols of each code length
653
+ 2. a list of symbols in canonical order
654
+
655
+ Note: the two lists may be used as input for `canonical_decode()`.
656
+ """
657
+ _check_anydict(__freq_map)
658
+
659
+ if len(__freq_map) == 1:
660
+ # Only one symbol: see note above in huffman_code()
661
+ sym = next(iter(__freq_map))
662
+ return {sym: frozenbitarray('0', 'big')}, [0, 1], [sym]
663
+
664
+ code_length = {} # map symbols to their code length
665
+
666
+ def traverse(nd, length):
667
+ # traverse the Huffman tree, but (unlike in huffman_code() above) we
668
+ # simply record the length for reaching each symbol
669
+ try: # leaf
670
+ code_length[nd.symbol] = length
671
+ except AttributeError: # parent, so traverse each child
672
+ traverse(nd.child[0], length + 1)
673
+ traverse(nd.child[1], length + 1)
674
+
675
+ traverse(_huffman_tree(__freq_map), 0)
676
+
677
+ # We now have a mapping of symbols to their code length, which is all we
678
+ # need to construct a list of tuples (symbol, code length) sorted by
679
+ # code length:
680
+ table = sorted(code_length.items(), key=lambda item: item[1])
681
+
682
+ maxbits = table[-1][1]
683
+ codedict = {}
684
+ count = (maxbits + 1) * [0]
685
+
686
+ code = 0
687
+ for i, (sym, length) in enumerate(table):
688
+ codedict[sym] = frozenbitarray(int2ba(code, length, 'big'))
689
+ count[length] += 1
690
+ if i + 1 < len(table):
691
+ code += 1
692
+ code <<= table[i + 1][1] - length
693
+
694
+ return codedict, count, [item[0] for item in table]