color/cache_key.rs
1// Copyright 2024 the Color Authors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! Hashing and other caching utilities for Color types.
5//!
6//! In this crate, colors are implemented using `f32`.
7//! This means that color types aren't `Hash` and `Eq` for good reasons:
8//!
9//! - Equality on these types is not reflexive (consider [NaN](f32::NAN)).
10//! - Certain values have two representations (`-0` and `+0` are both zero).
11//!
12//! However, it is still useful to create caches which key off these values.
13//! These are caches which don't have any semantic meaning, but instead
14//! are used to avoid redundant calculations or storage.
15//!
16//! Color supports creating these caches by using [`CacheKey<T>`] as the key in
17//! your cache.
18//! `T` is the key type (i.e. a color) which you want to use as the key.
19//! This `T` must implement both [`BitHash`] and [`BitEq`], which are
20//! versions of the standard `Hash` and `Eq` traits which support implementations
21//! for floating point numbers which might be unexpected outside of a caching context.
22
23use core::hash::{Hash, Hasher};
24
25/// A key usable in a hashmap to compare the bit representation
26/// types containing colors.
27///
28/// See the [module level docs](self) for more information.
29#[derive(Debug, Copy, Clone)]
30#[repr(transparent)]
31pub struct CacheKey<T>(pub T);
32
33impl<T> CacheKey<T> {
34 /// Create a new `CacheKey`.
35 ///
36 /// All fields are public, so the struct constructor can also be used.
37 pub fn new(value: T) -> Self {
38 Self(value)
39 }
40
41 /// Get the inner value.
42 pub fn into_inner(self) -> T {
43 self.0
44 }
45}
46
47// This module exists for these implementations:
48
49// `BitEq` is an equivalence relation, just maybe not the one you'd expect.
50impl<T: BitEq> Eq for CacheKey<T> {}
51impl<T: BitEq> PartialEq for CacheKey<T> {
52 fn eq(&self, other: &Self) -> bool {
53 self.0.bit_eq(&other.0)
54 }
55}
56// If we implement Eq, BitEq's implementation matches that of the hash.
57impl<T: BitHash> Hash for CacheKey<T> {
58 fn hash<H: Hasher>(&self, state: &mut H) {
59 self.0.bit_hash(state);
60 }
61}
62
63/// A hash implementation for types which normally wouldn't have one,
64/// implemented using a hash of the bitwise equivalent types when needed.
65///
66/// If a type is `BitHash` and `BitEq`, then it is important that the following property holds:
67///
68/// ```text
69/// k1 biteq k2 -> bithash(k1) == bithash(k2)
70/// ```
71///
72/// See the docs on [`Hash`] for more information.
73///
74/// Useful for creating caches based on exact values.
75/// See the [module level docs](self) for more information.
76pub trait BitHash {
77 /// Feeds this value into the given [`Hasher`].
78 fn bit_hash<H: Hasher>(&self, state: &mut H);
79 // Intentionally no hash_slice for simplicity.
80}
81
82impl BitHash for f32 {
83 fn bit_hash<H: Hasher>(&self, state: &mut H) {
84 self.to_bits().hash(state);
85 }
86}
87impl<T: BitHash, const N: usize> BitHash for [T; N] {
88 fn bit_hash<H: Hasher>(&self, state: &mut H) {
89 self[..].bit_hash(state);
90 }
91}
92
93impl<T: BitHash> BitHash for [T] {
94 fn bit_hash<H: Hasher>(&self, state: &mut H) {
95 // In theory, we should use `write_length_prefix`, which is unstable:
96 // https://github.com/rust-lang/rust/issues/96762
97 // We could do that by (unsafely) casting to `[CacheKey<T>]`, then
98 // using `Hash::hash` on the resulting slice.
99 state.write_usize(self.len());
100 for piece in self {
101 piece.bit_hash(state);
102 }
103 }
104}
105
106impl<T: BitHash> BitHash for &T {
107 fn bit_hash<H: Hasher>(&self, state: &mut H) {
108 T::bit_hash(*self, state);
109 }
110}
111
112// Don't BitHash tuples, not that important
113
114/// An equivalence relation for types which normally wouldn't have
115/// one, implemented using a bitwise comparison for floating point
116/// values.
117///
118/// See the docs on [`Eq`] for more information.
119///
120/// Useful for creating caches based on exact values.
121/// See the [module level docs](self) for more information.
122pub trait BitEq {
123 /// Returns `true` if `self` is equal to `other`.
124 ///
125 /// This need not use the semantically natural comparison operation
126 /// for the type; indeed floating point types should implement this
127 /// by comparing bit values.
128 fn bit_eq(&self, other: &Self) -> bool;
129 // Intentionally no bit_ne as would be added complexity for little gain
130}
131
132impl BitEq for f32 {
133 fn bit_eq(&self, other: &Self) -> bool {
134 self.to_bits() == other.to_bits()
135 }
136}
137
138impl<T: BitEq, const N: usize> BitEq for [T; N] {
139 fn bit_eq(&self, other: &Self) -> bool {
140 for i in 0..N {
141 if !self[i].bit_eq(&other[i]) {
142 return false;
143 }
144 }
145 true
146 }
147}
148
149impl<T: BitEq> BitEq for [T] {
150 fn bit_eq(&self, other: &Self) -> bool {
151 if self.len() != other.len() {
152 return false;
153 }
154 for (a, b) in self.iter().zip(other) {
155 if !a.bit_eq(b) {
156 return false;
157 }
158 }
159 true
160 }
161}
162
163impl<T: BitEq> BitEq for &T {
164 fn bit_eq(&self, other: &Self) -> bool {
165 T::bit_eq(*self, *other)
166 }
167}
168
169// Don't BitEq tuples, not that important
170
171// Ideally we'd also have these implementations, but they cause conflicts
172// (in case std ever went mad and implemented Eq for f32, for example).
173// impl<T: Hash> BitHash for T {...}
174// impl<T: PartialEq + Eq> BitEq for T {...}
175
176#[cfg(test)]
177mod tests {
178 extern crate std;
179 use super::CacheKey;
180 use crate::{parse_color, DynamicColor};
181 use std::collections::HashMap;
182
183 #[test]
184 fn bit_eq_hashmap() {
185 let mut map: HashMap<CacheKey<f32>, i32> = HashMap::new();
186 // The implementation for f32 is the base case.
187 assert!(map.insert(CacheKey(0.0), 0).is_none());
188 assert!(map.insert(CacheKey(-0.0), -1).is_none());
189 assert!(map.insert(CacheKey(1.0), 1).is_none());
190 assert!(map.insert(CacheKey(0.5), 5).is_none());
191
192 assert_eq!(map.get(&CacheKey(1.0)).unwrap(), &1);
193 assert_eq!(map.get(&CacheKey(0.0)).unwrap(), &0);
194 assert_eq!(map.remove(&CacheKey(-0.0)).unwrap(), -1);
195 assert!(!map.contains_key(&CacheKey(-0.0)));
196 assert_eq!(map.get(&CacheKey(0.5)).unwrap(), &5);
197 }
198 #[test]
199 fn bit_eq_color_hashmap() {
200 let mut map: HashMap<CacheKey<DynamicColor>, i32> = HashMap::new();
201
202 let red = parse_color("red").unwrap();
203 let red2 = parse_color("red").unwrap();
204 let other = parse_color("oklab(0.4 0.2 0.6)").unwrap();
205 assert!(map.insert(CacheKey(red), 10).is_none());
206 assert_eq!(map.insert(CacheKey(red2), 5).unwrap(), 10);
207 assert!(map.insert(CacheKey(other), 15).is_none());
208 assert_eq!(map.get(&CacheKey(other)).unwrap(), &15);
209 }
210}