use std::collections::HashMap;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DictionaryError {
CardinalityOverflow,
StringStorageOverflow,
}
impl std::fmt::Display for DictionaryError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::CardinalityOverflow => formatter.write_str("dictionary cardinality exceeds u32"),
Self::StringStorageOverflow => formatter.write_str("string storage exceeds u32"),
}
}
}
impl std::error::Error for DictionaryError {}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum CodeWidth {
U16,
U32,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum DictionaryCodes {
U16(Vec<u16>),
U32(Vec<u32>),
}
impl DictionaryCodes {
pub(crate) fn resident_bytes(&self) -> Option<usize> {
match self {
Self::U16(codes) => codes.capacity().checked_mul(std::mem::size_of::<u16>()),
Self::U32(codes) => codes.capacity().checked_mul(std::mem::size_of::<u32>()),
}
}
#[must_use]
pub const fn width(&self) -> CodeWidth {
match self {
Self::U16(_) => CodeWidth::U16,
Self::U32(_) => CodeWidth::U32,
}
}
#[must_use]
pub fn len(&self) -> usize {
match self {
Self::U16(codes) => codes.len(),
Self::U32(codes) => codes.len(),
}
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.len() == 0
}
#[must_use]
pub fn get(&self, row: usize) -> Option<u32> {
match self {
Self::U16(codes) => codes.get(row).copied().map(u32::from),
Self::U32(codes) => codes.get(row).copied(),
}
}
pub(crate) fn iter(&self) -> impl Iterator<Item = u32> + '_ {
enum Iter<'a> {
U16(std::slice::Iter<'a, u16>),
U32(std::slice::Iter<'a, u32>),
}
impl Iterator for Iter<'_> {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
match self {
Iter::U16(values) => values.next().copied().map(u32::from),
Iter::U32(values) => values.next().copied(),
}
}
}
match self {
Self::U16(values) => Iter::U16(values.iter()),
Self::U32(values) => Iter::U32(values.iter()),
}
}
pub(crate) fn from_u32(codes: Vec<u32>, cardinality: u64) -> Result<Self, DictionaryError> {
match code_width_for_cardinality(cardinality)? {
CodeWidth::U16 => {
let narrowed = codes
.into_iter()
.map(u16::try_from)
.collect::<Result<Vec<_>, _>>()
.map_err(|_| DictionaryError::CardinalityOverflow)?;
Ok(Self::U16(narrowed))
}
CodeWidth::U32 => Ok(Self::U32(codes)),
}
}
}
pub(crate) fn code_width_for_cardinality(cardinality: u64) -> Result<CodeWidth, DictionaryError> {
if cardinality <= u64::from(u16::MAX) {
Ok(CodeWidth::U16)
} else if cardinality <= u64::from(u32::MAX) {
Ok(CodeWidth::U32)
} else {
Err(DictionaryError::CardinalityOverflow)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[repr(C)]
pub(crate) struct StringStorage {
offsets: Vec<u32>,
bytes: Vec<u8>,
}
impl StringStorage {
pub(crate) fn resident_bytes(&self) -> Option<usize> {
self.offsets
.capacity()
.checked_mul(std::mem::size_of::<u32>())?
.checked_add(self.bytes.capacity())
}
pub(crate) fn new() -> Self {
Self {
offsets: vec![0],
bytes: Vec::new(),
}
}
pub(crate) fn can_push(&self, value: &str) -> Result<(), DictionaryError> {
let new_length = self
.bytes
.len()
.checked_add(value.len())
.ok_or(DictionaryError::StringStorageOverflow)?;
u32::try_from(new_length)
.map(|_| ())
.map_err(|_| DictionaryError::StringStorageOverflow)
}
pub(crate) fn push(&mut self, value: &str) -> Result<(), DictionaryError> {
self.can_push(value)?;
self.bytes.extend_from_slice(value.as_bytes());
let offset =
u32::try_from(self.bytes.len()).map_err(|_| DictionaryError::StringStorageOverflow)?;
self.offsets.push(offset);
Ok(())
}
pub(crate) fn len(&self) -> usize {
self.offsets.len().saturating_sub(1)
}
#[cfg(any(test, feature = "test-support"))]
pub(crate) fn byte_len(&self) -> usize {
self.bytes.len()
}
pub(crate) fn get(&self, position: usize) -> Option<&str> {
let start = self
.offsets
.get(position)
.copied()
.map(|value| value as usize)?;
let end = self
.offsets
.get(position.saturating_add(1))
.copied()
.map(|value| value as usize)?;
let bytes = self.bytes.get(start..end)?;
std::str::from_utf8(bytes).ok()
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[repr(C)]
pub struct StringDictionary {
values: StringStorage,
}
impl StringDictionary {
pub(crate) fn resident_bytes(&self) -> Option<usize> {
self.values.resident_bytes()
}
#[must_use]
pub fn len(&self) -> usize {
self.values.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.len() == 0
}
#[must_use]
pub fn get(&self, code: u32) -> Option<&str> {
let position = usize::try_from(code).ok()?;
self.values.get(position)
}
#[must_use]
pub fn find(&self, needle: &str) -> Option<u32> {
self.values
.offsets
.windows(2)
.enumerate()
.find_map(|(position, _)| {
(self.values.get(position) == Some(needle))
.then(|| u32::try_from(position).ok())
.flatten()
})
}
}
pub(crate) struct DictionaryBuilder {
lookup: HashMap<String, u32>,
values: StringStorage,
}
impl DictionaryBuilder {
pub(crate) fn new() -> Self {
Self {
lookup: HashMap::new(),
values: StringStorage::new(),
}
}
pub(crate) fn can_intern(&self, value: &str) -> Result<(), DictionaryError> {
if self.lookup.contains_key(value) {
return Ok(());
}
let cardinality = u64::try_from(self.lookup.len())
.unwrap_or(u64::MAX)
.saturating_add(1);
code_width_for_cardinality(cardinality)?;
self.values.can_push(value)
}
pub(crate) fn intern(&mut self, value: &str) -> Result<u32, DictionaryError> {
if let Some(code) = self.lookup.get(value).copied() {
return Ok(code);
}
self.can_intern(value)?;
let code =
u32::try_from(self.lookup.len()).map_err(|_| DictionaryError::CardinalityOverflow)?;
self.values.push(value)?;
self.lookup.insert(value.to_owned(), code);
Ok(code)
}
pub(crate) fn cardinality(&self) -> u64 {
u64::try_from(self.lookup.len()).unwrap_or(u64::MAX)
}
#[cfg(any(test, feature = "test-support"))]
pub(crate) fn contains(&self, value: &str) -> bool {
self.lookup.contains_key(value)
}
#[cfg(any(test, feature = "test-support"))]
pub(crate) fn string_bytes(&self) -> usize {
self.values.byte_len()
}
pub(crate) fn finish(self) -> StringDictionary {
StringDictionary {
values: self.values,
}
}
}
#[allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
use proptest::test_runner::{Config, RngSeed, TestRunner};
use rand::RngCore;
#[test]
fn prop_dict_roundtrip() {
let name = "meta::dict::prop_dict_roundtrip";
let mut seeded = crate::test_support::seeded_rng(name);
let config = Config {
rng_seed: RngSeed::Fixed(seeded.next_u64()),
..Config::default()
};
let mut runner = TestRunner::new(config);
let values = prop::collection::vec("[a-z]{0,16}", 0..=256);
let result = runner.run(&values, |values| {
let mut builder = DictionaryBuilder::new();
let codes = values
.iter()
.map(|value| builder.intern(value).expect("bounded dictionary"))
.collect::<Vec<_>>();
let dictionary = builder.finish();
let decoded = codes
.into_iter()
.map(|code| dictionary.get(code).map(str::to_owned))
.collect::<Vec<_>>();
let expected = values.into_iter().map(Some).collect::<Vec<_>>();
prop_assert_eq!(decoded, expected);
Ok(())
});
assert!(result.is_ok(), "property result: {result:?}");
}
#[test]
fn dictionary_auto_widens_u16_to_u32() {
let cardinality = u64::from(u16::MAX).saturating_add(1);
let codes = (0..=u32::from(u16::MAX)).collect::<Vec<_>>();
let encoded = DictionaryCodes::from_u32(codes, cardinality).expect("u32 cardinality");
assert_eq!(encoded.width(), CodeWidth::U32);
}
#[test]
fn dictionary_reports_u32_cardinality_overflow() {
assert_eq!(
code_width_for_cardinality(u64::from(u32::MAX).saturating_add(1)),
Err(DictionaryError::CardinalityOverflow)
);
}
}