1use std::cell::RefCell;
46
47use rudb_common::{Error, Result};
48
49pub const ESCAPE: u8 = 255;
52
53pub const MAX_SYMBOLS: usize = 255;
55
56pub const MAX_SYMBOL_LEN: usize = 8;
59
60const GENERATIONS: usize = 5;
62
63const HASH_SLOTS: usize = 1024;
66
67const PROBE: usize = 8;
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
73struct Symbol {
74 value: u64,
75 len: u8,
76}
77
78impl Symbol {
79 fn new(bytes: &[u8]) -> Self {
80 let len = bytes.len().min(MAX_SYMBOL_LEN);
81 let mut value = 0u64;
82 for (index, byte) in bytes[..len].iter().enumerate() {
83 value |= u64::from(*byte) << (8 * index);
84 }
85 Self { value, len: len as u8 }
86 }
87
88 fn single(byte: u8) -> Self {
89 Self { value: u64::from(byte), len: 1 }
90 }
91
92 fn len(self) -> usize {
93 self.len as usize
94 }
95
96 fn mask(self) -> u64 {
97 mask_of(self.len())
98 }
99
100 fn bytes(self) -> Vec<u8> {
101 (0..self.len()).map(|index| (self.value >> (8 * index)) as u8).collect()
102 }
103
104 fn concat(self, other: Self) -> Self {
106 if self.len() >= MAX_SYMBOL_LEN {
107 return self;
108 }
109 let len = (self.len() + other.len()).min(MAX_SYMBOL_LEN);
110 let value = self.value | (other.value << (8 * self.len()));
111 Self { value: value & mask_of(len), len: len as u8 }
112 }
113}
114
115fn mask_of(len: usize) -> u64 {
116 if len >= 8 { u64::MAX } else { (1u64 << (8 * len)) - 1 }
117}
118
119pub struct SymbolTable {
121 symbols: Vec<Symbol>,
123 single: Vec<u8>,
125 pair: Vec<u16>,
127 hash: Vec<Option<(Symbol, u8)>>,
129}
130
131impl std::fmt::Debug for SymbolTable {
132 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
133 formatter
135 .debug_struct("SymbolTable")
136 .field("symbols", &self.symbols.len())
137 .field("bytes", &self.serialized_len())
138 .finish()
139 }
140}
141
142impl PartialEq for SymbolTable {
149 fn eq(&self, other: &Self) -> bool {
150 self.symbols == other.symbols
151 }
152}
153
154impl Eq for SymbolTable {}
155
156impl SymbolTable {
157 #[must_use]
164 pub fn footprint(&self) -> usize {
165 size_of::<Self>()
166 + self.symbols.capacity() * size_of::<Symbol>()
167 + self.single.capacity()
168 + self.pair.capacity() * size_of::<u16>()
169 + self.hash.capacity() * size_of::<Option<(Symbol, u8)>>()
170 }
171
172 #[must_use]
175 pub fn empty() -> Self {
176 Self::build(Vec::new())
177 }
178
179 #[must_use]
185 pub fn train(samples: &[&[u8]]) -> Self {
186 thread_local! {
190 static COUNTS: RefCell<Option<Counts>> = const { RefCell::new(None) };
191 }
192 COUNTS.with(|held| match held.try_borrow_mut() {
193 Ok(mut held) => Self::train_with(samples, held.get_or_insert_with(Counts::new)),
194 Err(_) => Self::train_with(samples, &mut Counts::new()),
195 })
196 }
197
198 fn train_with(samples: &[&[u8]], counts: &mut Counts) -> Self {
199 let mut table = Self::empty();
200 for _ in 0..GENERATIONS {
201 counts.clear();
202 for sample in samples {
203 table.count(sample, counts);
204 }
205 let next = counts.best(&table);
206 if next.is_empty() {
207 break;
208 }
209 table = Self::build(next);
210 }
211 table
212 }
213
214 #[must_use]
216 pub fn len(&self) -> usize {
217 self.symbols.len()
218 }
219
220 #[must_use]
222 pub fn is_empty(&self) -> bool {
223 self.symbols.is_empty()
224 }
225
226 #[must_use]
230 pub fn serialized_len(&self) -> usize {
231 1 + self.symbols.iter().map(|symbol| 1 + symbol.len()).sum::<usize>()
232 }
233
234 pub fn serialize(&self, out: &mut Vec<u8>) {
236 out.push(self.symbols.len() as u8);
237 for symbol in &self.symbols {
238 out.push(symbol.len);
239 out.extend_from_slice(&symbol.bytes());
240 }
241 }
242
243 pub fn deserialize(bytes: &[u8]) -> Result<(Self, usize)> {
249 let count = *bytes.first().ok_or_else(|| truncated("a symbol table header"))? as usize;
250 let mut at = 1;
251 let mut symbols = Vec::with_capacity(count);
252 for _ in 0..count {
253 let len = *bytes.get(at).ok_or_else(|| truncated("a symbol length"))? as usize;
254 if len == 0 || len > MAX_SYMBOL_LEN {
255 return Err(Error::internal(format!("a symbol of {len} bytes is not a symbol")));
256 }
257 at += 1;
258 let end = at + len;
259 if end > bytes.len() {
260 return Err(truncated("a symbol"));
261 }
262 symbols.push(Symbol::new(&bytes[at..end]));
263 at = end;
264 }
265 Ok((Self::build(symbols), at))
266 }
267
268 pub fn compress(&self, input: &[u8], out: &mut Vec<u8>) {
274 let mut at = 0;
275 while at < input.len() {
276 let (code, len) = self.match_at(input, at);
277 if code == ESCAPE {
278 out.push(ESCAPE);
279 out.push(input[at]);
280 } else {
281 out.push(code);
282 }
283 at += len;
284 }
285 }
286
287 pub fn decompress(&self, input: &[u8], out: &mut Vec<u8>) -> Result<()> {
304 out.reserve(input.len().saturating_mul(2).saturating_add(MAX_SYMBOL_LEN));
308 let mut at = 0;
309 while at < input.len() {
310 let code = input[at];
311 at += 1;
312 if code == ESCAPE {
313 let literal = *input.get(at).ok_or_else(|| truncated("an escaped byte"))?;
314 out.push(literal);
315 at += 1;
316 } else {
317 let symbol = *self
318 .symbols
319 .get(code as usize)
320 .ok_or_else(|| Error::internal(format!("code {code} is not in the table")))?;
321 out.extend_from_slice(&symbol.value.to_le_bytes());
322 out.truncate(out.len() - (MAX_SYMBOL_LEN - symbol.len()));
323 }
324 }
325 Ok(())
326 }
327
328 fn match_at(&self, input: &[u8], at: usize) -> (u8, usize) {
330 let remaining = input.len() - at;
331 let word = load(input, at);
332 if remaining >= 3 {
335 if let Some((symbol, code)) = self.probe(word, remaining) {
336 return (code, symbol.len());
337 }
338 }
339 if remaining >= 2 {
340 let code = self.pair[(word & 0xffff) as usize];
341 if code != u16::MAX {
342 return (code as u8, 2);
343 }
344 }
345 let code = self.single[(word & 0xff) as usize];
346 if code == ESCAPE { (ESCAPE, 1) } else { (code, 1) }
347 }
348
349 fn probe(&self, word: u64, remaining: usize) -> Option<(Symbol, u8)> {
355 let mut slot = hash_of(word);
356 let mut best: Option<(Symbol, u8)> = None;
357 for _ in 0..PROBE {
358 match self.hash[slot] {
359 None => break,
360 Some((symbol, code)) => {
361 if symbol.len() <= remaining
362 && word & symbol.mask() == symbol.value
363 && best.is_none_or(|(found, _)| symbol.len() > found.len())
364 {
365 best = Some((symbol, code));
366 }
367 }
368 }
369 slot = (slot + 1) & (HASH_SLOTS - 1);
370 }
371 best
372 }
373
374 fn count(&self, input: &[u8], counts: &mut Counts) {
377 let mut at = 0;
378 let mut previous: Option<u16> = None;
379 while at < input.len() {
380 let (code, len) = self.match_at(input, at);
381 let id = if code == ESCAPE { 256 + u16::from(input[at]) } else { u16::from(code) };
382 counts.one(id);
383 if let Some(previous) = previous {
384 counts.two(previous, id);
385 }
386 previous = Some(id);
387 at += len;
388 }
389 }
390
391 fn build(symbols: Vec<Symbol>) -> Self {
392 let mut table = Self {
393 symbols,
394 single: vec![ESCAPE; 256],
395 pair: vec![u16::MAX; 65536],
396 hash: vec![None; HASH_SLOTS],
397 };
398 let mut order: Vec<(Symbol, u8)> =
401 table.symbols.iter().enumerate().map(|(code, symbol)| (*symbol, code as u8)).collect();
402 order.sort_by_key(|(symbol, code)| (std::cmp::Reverse(symbol.len()), *code));
403 for (symbol, code) in order {
404 match symbol.len() {
405 1 => {
406 let index = (symbol.value & 0xff) as usize;
407 if table.single[index] == ESCAPE {
408 table.single[index] = code;
409 }
410 }
411 2 => {
412 let index = (symbol.value & 0xffff) as usize;
413 if table.pair[index] == u16::MAX {
414 table.pair[index] = u16::from(code);
415 }
416 }
417 _ => {
418 let mut slot = hash_of(symbol.value);
419 for _ in 0..PROBE {
420 if table.hash[slot].is_none() {
421 table.hash[slot] = Some((symbol, code));
422 break;
423 }
424 slot = (slot + 1) & (HASH_SLOTS - 1);
425 }
426 }
427 }
428 }
429 table
430 }
431}
432
433fn load(input: &[u8], at: usize) -> u64 {
438 if at + 8 <= input.len() {
439 let bytes: [u8; 8] = input[at..at + 8].try_into().expect("eight bytes were checked");
440 u64::from_le_bytes(bytes)
441 } else {
442 let mut word = 0u64;
443 for (index, byte) in input[at..].iter().enumerate() {
444 word |= u64::from(*byte) << (8 * index);
445 }
446 word
447 }
448}
449
450fn hash_of(word: u64) -> usize {
454 let key = word & 0xff_ffff;
455 ((key.wrapping_mul(0x9e37_79b9_7f4a_7c15)) >> (64 - HASH_SLOTS.trailing_zeros())) as usize
456}
457
458struct Counts {
462 single: Vec<u32>,
463 pairs: Vec<u32>,
466 seen: Vec<u32>,
469 gains: Gains,
471}
472
473#[derive(Default)]
475struct Gains {
476 gains: Vec<(Symbol, u64)>,
477 places: Vec<u32>,
479 taken: Vec<u32>,
481}
482
483impl Gains {
484 fn clear(&mut self, most: usize) {
486 for place in self.taken.drain(..) {
487 self.places[place as usize] = u32::MAX;
488 }
489 let wanted = (most * 2).next_power_of_two();
490 if self.places.len() < wanted {
491 self.places = vec![u32::MAX; wanted];
492 }
493 self.gains.clear();
494 }
495
496 fn add(&mut self, symbol: Symbol, count: u64) {
498 let gain = count * symbol.len() as u64;
499 let mask = self.places.len() - 1;
500 let mut place = gain_hash(symbol) & mask;
501 loop {
502 let at = self.places[place];
503 if at == u32::MAX {
504 self.places[place] = self.gains.len() as u32;
505 self.taken.push(place as u32);
506 self.gains.push((symbol, gain));
507 return;
508 }
509 if self.gains[at as usize].0 == symbol {
510 self.gains[at as usize].1 += gain;
511 return;
512 }
513 place = (place + 1) & mask;
514 }
515 }
516}
517
518const IDS: usize = 512;
520
521impl Counts {
522 fn new() -> Self {
523 Self {
524 single: vec![0; IDS],
525 pairs: vec![0; IDS * IDS],
526 seen: Vec::new(),
527 gains: Gains::default(),
528 }
529 }
530
531 fn clear(&mut self) {
532 self.single.fill(0);
533 for slot in self.seen.drain(..) {
534 self.pairs[slot as usize] = 0;
535 }
536 }
537
538 fn one(&mut self, id: u16) {
539 self.single[id as usize] += 1;
540 }
541
542 fn two(&mut self, first: u16, second: u16) {
543 let slot = first as usize * IDS + second as usize;
544 if self.pairs[slot] == 0 {
545 self.seen.push(slot as u32);
546 }
547 self.pairs[slot] += 1;
548 }
549
550 fn best(&mut self, table: &SymbolTable) -> Vec<Symbol> {
557 self.gains.clear(IDS + self.seen.len());
562 for (id, count) in self.single.iter().enumerate() {
563 if *count == 0 {
564 continue;
565 }
566 let symbol = symbol_of(table, id as u16);
567 self.gains.add(symbol, u64::from(*count));
568 }
569 for slot in &self.seen {
570 let slot = *slot as usize;
571 let (first, second) = ((slot / IDS) as u16, (slot % IDS) as u16);
572 let symbol = symbol_of(table, first).concat(symbol_of(table, second));
573 self.gains.add(symbol, u64::from(self.pairs[slot]));
574 }
575 let gains = &mut self.gains.gains;
576 let order = |left: &(Symbol, u64), right: &(Symbol, u64)| {
580 right.1.cmp(&left.1).then(left.0.cmp(&right.0))
581 };
582 if gains.len() > MAX_SYMBOLS {
583 gains.select_nth_unstable_by(MAX_SYMBOLS - 1, order);
584 gains.truncate(MAX_SYMBOLS);
585 }
586 gains.sort_unstable_by(order);
587 gains.iter().map(|(symbol, _)| *symbol).collect()
588 }
589}
590
591fn gain_hash(symbol: Symbol) -> usize {
593 ((symbol.value ^ u64::from(symbol.len)).wrapping_mul(0x9e37_79b9_7f4a_7c15) >> 32) as usize
594}
595
596fn symbol_of(table: &SymbolTable, id: u16) -> Symbol {
597 if (id as usize) < table.symbols.len() {
598 table.symbols[id as usize]
599 } else {
600 Symbol::single((id.saturating_sub(256)) as u8)
601 }
602}
603
604fn truncated(what: &str) -> Error {
605 Error::internal(format!("the input ended in the middle of {what}"))
606}
607
608#[cfg(test)]
609mod tests {
610 use super::*;
611
612 fn urls() -> Vec<Vec<u8>> {
616 let hosts = ["www.example.com", "shop.example.com", "news.other.example.org"];
617 let paths = ["/index.html", "/catalog/item", "/search", "/user/profile/settings"];
618 let mut out = Vec::new();
619 for index in 0..600 {
620 let host = hosts[index % hosts.len()];
621 let path = paths[(index / 3) % paths.len()];
622 out.push(
623 format!("http://{host}{path}?session={}&ref=google&page={}", index * 7, index % 20)
624 .into_bytes(),
625 );
626 }
627 out
628 }
629
630 fn borrow(strings: &[Vec<u8>]) -> Vec<&[u8]> {
631 strings.iter().map(Vec::as_slice).collect()
632 }
633
634 fn round_trip(table: &SymbolTable, strings: &[Vec<u8>]) -> (usize, usize) {
635 let mut raw = 0;
636 let mut compressed = 0;
637 for string in strings {
638 let mut bytes = Vec::new();
639 table.compress(string, &mut bytes);
640 let mut back = Vec::new();
641 table.decompress(&bytes, &mut back).unwrap();
642 assert_eq!(back, *string, "{}", String::from_utf8_lossy(string));
643 raw += string.len();
644 compressed += bytes.len();
645 }
646 (raw, compressed)
647 }
648
649 #[test]
650 fn urls_compress_by_more_than_half_and_come_back_unchanged() {
651 let strings = urls();
654 let table = SymbolTable::train(&borrow(&strings));
655 let (raw, compressed) = round_trip(&table, &strings);
656 let ratio = raw as f64 / compressed as f64;
657 assert!(ratio > 2.5, "{ratio:.2}x, {raw} to {compressed}");
658 assert!(table.len() > 100, "{} symbols", table.len());
659 }
660
661 #[test]
662 fn the_trainer_finds_the_long_repeated_pieces() {
663 let strings = urls();
664 let table = SymbolTable::train(&borrow(&strings));
665 let found: Vec<String> = (0..table.len())
666 .map(|code| String::from_utf8_lossy(&table.symbols[code].bytes()).into_owned())
667 .collect();
668 let long = found.iter().filter(|symbol| symbol.len() >= 6).count();
671 assert!(long > 60, "only {long} symbols of six bytes or more: {found:?}");
672 }
673
674 #[test]
675 fn english_text_round_trips_and_shrinks() {
676 let text: Vec<Vec<u8>> = "the quick brown fox jumps over the lazy dog while the other dog \
677 watches the fox and the dog and the fox go over the hill together"
678 .split(' ')
679 .map(|word| word.as_bytes().to_vec())
680 .collect();
681 let table = SymbolTable::train(&borrow(&text));
682 let (raw, compressed) = round_trip(&table, &text);
683 assert!(compressed < raw, "{raw} to {compressed}");
684 }
685
686 #[test]
687 fn incompressible_bytes_round_trip_and_cost_what_escaping_costs() {
688 let mut state = 0x1234_5678_9abc_def0u64;
692 let strings: Vec<Vec<u8>> = (0..100)
693 .map(|_| {
694 (0..64)
695 .map(|_| {
696 state ^= state << 13;
697 state ^= state >> 7;
698 state ^= state << 17;
699 state as u8
700 })
701 .collect()
702 })
703 .collect();
704 let table = SymbolTable::train(&borrow(&strings));
705 let (raw, compressed) = round_trip(&table, &strings);
706 assert!(compressed < raw * 2, "{raw} to {compressed}");
707 }
708
709 #[test]
710 fn an_empty_table_escapes_everything_and_still_round_trips() {
711 let table = SymbolTable::empty();
712 let strings = vec![b"hello".to_vec(), Vec::new(), b"x".to_vec()];
713 let (raw, compressed) = round_trip(&table, &strings);
714 assert_eq!(compressed, raw * 2);
715 }
716
717 #[test]
718 fn an_empty_string_compresses_to_nothing() {
719 let table = SymbolTable::train(&[b"abcabcabc"]);
720 let mut out = Vec::new();
721 table.compress(b"", &mut out);
722 assert!(out.is_empty());
723 let mut back = Vec::new();
724 table.decompress(&out, &mut back).unwrap();
725 assert!(back.is_empty());
726 }
727
728 #[test]
729 fn a_string_shorter_than_the_symbols_does_not_read_past_its_end() {
730 let table = SymbolTable::train(&[b"ab\0ab\0ab\0ab\0", b"abcdefgh"]);
733 for string in [b"a".to_vec(), b"ab".to_vec(), b"abc".to_vec()] {
734 let mut bytes = Vec::new();
735 table.compress(&string, &mut bytes);
736 let mut back = Vec::new();
737 table.decompress(&bytes, &mut back).unwrap();
738 assert_eq!(back, string);
739 }
740 }
741
742 #[test]
743 fn a_table_survives_being_written_and_read_back() {
744 let strings = urls();
745 let table = SymbolTable::train(&borrow(&strings));
746 let mut bytes = Vec::new();
747 table.serialize(&mut bytes);
748 assert_eq!(bytes.len(), table.serialized_len());
749 let (read, consumed) = SymbolTable::deserialize(&bytes).unwrap();
750 assert_eq!(consumed, bytes.len());
751 assert_eq!(read.symbols, table.symbols);
752
753 let mut first = Vec::new();
756 let mut second = Vec::new();
757 table.compress(&strings[7], &mut first);
758 read.compress(&strings[7], &mut second);
759 assert_eq!(first, second);
760 }
761
762 #[test]
763 fn a_full_table_is_two_kilobytes_at_the_very_most() {
764 let strings = urls();
765 let table = SymbolTable::train(&borrow(&strings));
766 assert!(table.serialized_len() <= 1 + MAX_SYMBOLS * (1 + MAX_SYMBOL_LEN));
767 assert!(table.serialized_len() <= 2049);
768 }
769
770 #[test]
771 fn a_truncated_symbol_table_is_an_error() {
772 let strings = urls();
773 let table = SymbolTable::train(&borrow(&strings));
774 let mut bytes = Vec::new();
775 table.serialize(&mut bytes);
776 for len in 1..bytes.len().min(40) {
777 let error = SymbolTable::deserialize(&bytes[..len]).unwrap_err();
778 assert!(error.message().contains("ended in the middle"), "{error}");
779 }
780 }
781
782 #[test]
783 fn a_symbol_of_zero_bytes_is_an_error() {
784 let error = SymbolTable::deserialize(&[1, 0]).unwrap_err();
785 assert!(error.message().contains("is not a symbol"), "{error}");
786 }
787
788 #[test]
789 fn a_short_symbol_does_not_drag_the_rest_of_its_word_out_with_it() {
790 let table = SymbolTable::train(&[b"abcdabcdabcdabcd"]);
796 let mut compressed = Vec::new();
797 table.compress(b"abcdabcd", &mut compressed);
798 let mut out = Vec::new();
799 table.decompress(&compressed, &mut out).expect("decompresses");
800 table.decompress(&compressed, &mut out).expect("decompresses");
801 assert_eq!(out, b"abcdabcdabcdabcd");
802 }
803
804 #[test]
805 fn a_dangling_escape_is_an_error_and_not_a_panic() {
806 let table = SymbolTable::train(&[b"abcabcabc"]);
807 let error = table.decompress(&[ESCAPE], &mut Vec::new()).unwrap_err();
808 assert!(error.message().contains("escaped byte"), "{error}");
809 }
810
811 #[test]
812 fn a_code_the_table_does_not_have_is_an_error() {
813 let table = SymbolTable::train(&[b"abcabcabc"]);
814 let code = table.len() as u8;
815 let error = table.decompress(&[code], &mut Vec::new()).unwrap_err();
816 assert!(error.message().contains("not in the table"), "{error}");
817 }
818
819 #[test]
820 fn training_twice_on_the_same_sample_gives_the_same_table() {
821 let strings = urls();
823 let first = SymbolTable::train(&borrow(&strings));
824 let second = SymbolTable::train(&borrow(&strings));
825 assert_eq!(first.symbols, second.symbols);
826 }
827
828 #[test]
829 fn the_longest_match_wins_rather_than_the_first_one_found() {
830 let table = SymbolTable::build(vec![
831 Symbol::new(b"abc"),
832 Symbol::new(b"abcdef"),
833 Symbol::new(b"abcd"),
834 ]);
835 let mut out = Vec::new();
836 table.compress(b"abcdef", &mut out);
837 assert_eq!(out, vec![1]);
838 }
839
840 #[test]
841 fn a_symbol_longer_than_what_is_left_is_not_used() {
842 let table = SymbolTable::build(vec![Symbol::new(b"abcdef"), Symbol::new(b"ab")]);
843 let mut out = Vec::new();
844 table.compress(b"abcd", &mut out);
845 assert_eq!(out, vec![1, ESCAPE, b'c', ESCAPE, b'd']);
847 }
848
849 #[test]
850 fn concatenation_stops_at_eight_bytes() {
851 let long = Symbol::new(b"abcdef");
852 assert_eq!(long.concat(Symbol::new(b"ghijkl")).bytes(), b"abcdefgh");
853 assert_eq!(Symbol::new(b"ab").concat(Symbol::new(b"cd")).bytes(), b"abcd");
854 }
855
856 fn train_with_maps(samples: &[&[u8]]) -> SymbolTable {
859 use std::collections::HashMap;
860 let mut table = SymbolTable::empty();
861 for _ in 0..GENERATIONS {
862 let mut single = [0u32; IDS];
863 let mut pairs: HashMap<(u16, u16), u32> = HashMap::new();
864 for sample in samples {
865 let mut at = 0;
866 let mut previous: Option<u16> = None;
867 while at < sample.len() {
868 let (code, len) = table.match_at(sample, at);
869 let id =
870 if code == ESCAPE { 256 + u16::from(sample[at]) } else { u16::from(code) };
871 single[id as usize] += 1;
872 if let Some(previous) = previous {
873 *pairs.entry((previous, id)).or_insert(0) += 1;
874 }
875 previous = Some(id);
876 at += len;
877 }
878 }
879 let mut gains: HashMap<Symbol, u64> = HashMap::new();
880 for (id, count) in single.iter().enumerate().filter(|(_, count)| **count > 0) {
881 let symbol = symbol_of(&table, id as u16);
882 *gains.entry(symbol).or_insert(0) += u64::from(*count) * symbol.len() as u64;
883 }
884 for ((first, second), count) in &pairs {
885 let symbol = symbol_of(&table, *first).concat(symbol_of(&table, *second));
886 *gains.entry(symbol).or_insert(0) += u64::from(*count) * symbol.len() as u64;
887 }
888 let mut ranked: Vec<(Symbol, u64)> = gains.into_iter().collect();
889 ranked.sort_by(|left, right| right.1.cmp(&left.1).then(left.0.cmp(&right.0)));
890 ranked.truncate(MAX_SYMBOLS);
891 if ranked.is_empty() {
892 break;
893 }
894 table = SymbolTable::build(ranked.into_iter().map(|(symbol, _)| symbol).collect());
895 }
896 table
897 }
898
899 #[test]
900 fn flat_counts_train_the_same_table_as_hash_maps() {
901 let mut state = 0x9e37_79b9_7f4a_7c15u64;
902 let mut next = move || {
903 state ^= state << 13;
904 state ^= state >> 7;
905 state ^= state << 17;
906 state
907 };
908 let mut shapes: Vec<Vec<Vec<u8>>> = vec![urls(), Vec::new(), vec![Vec::new(); 3]];
909 shapes.push(
911 (0..400).map(|_| (0..12).map(|_| b"abc"[(next() % 3) as usize]).collect()).collect(),
912 );
913 shapes.push((0..300).map(|_| (0..40).map(|_| next() as u8).collect()).collect());
915 shapes.push(
917 (0..200)
918 .map(|index| format!("prefix-{}-suffix-{}", index % 7, index % 5).into_bytes())
919 .collect(),
920 );
921 for strings in &shapes {
924 let samples = borrow(strings);
925 assert_eq!(SymbolTable::train(&samples).symbols, train_with_maps(&samples).symbols);
926 }
927 }
928}