mfsk_core/msg/
hash_table.rs1use std::collections::HashMap;
16
17const C38: &[u8] = b" 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ/";
19
20const HASH_MAGIC: u64 = 47_055_833_459;
22
23const MAX_HASH22: usize = 1000;
25
26pub fn ihashcall(call: &str, m: u32) -> u32 {
36 let call = call.to_ascii_uppercase();
37 let bytes = call.as_bytes();
38
39 let mut n64: u64 = 0;
40 for i in 0..11 {
41 let c = if i < bytes.len() { bytes[i] } else { b' ' };
42 let j = C38.iter().position(|&x| x == c).unwrap_or(0);
43 n64 = n64.wrapping_mul(38).wrapping_add(j as u64);
44 }
45
46 let hash64 = n64.wrapping_mul(HASH_MAGIC);
47 (hash64 >> (64 - m)) as u32
48}
49
50#[derive(Debug, Clone)]
55pub struct CallsignHashTable {
56 hash10: HashMap<u32, String>,
58 hash12: HashMap<u32, String>,
60 hash22: Vec<(u32, String)>,
62}
63
64impl CallsignHashTable {
65 pub fn new() -> Self {
67 Self {
68 hash10: HashMap::new(),
69 hash12: HashMap::new(),
70 hash22: Vec::new(),
71 }
72 }
73
74 pub fn insert(&mut self, call: &str) {
79 let call = call.trim();
80 let call = call.strip_prefix('<').unwrap_or(call);
82 let call = call.strip_suffix('>').unwrap_or(call);
83 let base = if call.ends_with("/R") || call.ends_with("/P") {
85 &call[..call.len() - 2]
86 } else {
87 call
88 };
89
90 if base.len() < 2 || base == "..." || base.starts_with("CQ") {
91 return;
92 }
93
94 let n10 = ihashcall(base, 10);
95 let n12 = ihashcall(base, 12);
96 let n22 = ihashcall(base, 22);
97
98 self.hash10.insert(n10, base.to_string());
99 self.hash12.insert(n12, base.to_string());
100
101 if let Some(pos) = self.hash22.iter().position(|(h, _)| *h == n22) {
103 let entry = self.hash22.remove(pos);
105 self.hash22.insert(0, entry);
106 } else {
107 self.hash22.insert(0, (n22, base.to_string()));
108 if self.hash22.len() > MAX_HASH22 {
109 self.hash22.pop();
110 }
111 }
112 }
113
114 pub fn lookup10(&self, n10: u32) -> Option<&str> {
116 self.hash10.get(&n10).map(|s| s.as_str())
117 }
118
119 pub fn lookup12(&self, n12: u32) -> Option<&str> {
121 self.hash12.get(&n12).map(|s| s.as_str())
122 }
123
124 pub fn lookup22(&self, n22: u32) -> Option<String> {
127 self.hash22
128 .iter()
129 .find(|(h, _)| *h == n22)
130 .map(|(_, call)| format!("<{}>", call))
131 }
132
133 pub fn clear(&mut self) {
135 self.hash10.clear();
136 self.hash12.clear();
137 self.hash22.clear();
138 }
139
140 pub fn len22(&self) -> usize {
142 self.hash22.len()
143 }
144}
145
146impl Default for CallsignHashTable {
147 fn default() -> Self {
148 Self::new()
149 }
150}
151
152#[cfg(test)]
155mod tests {
156 use super::*;
157
158 #[test]
159 fn hash_basic() {
160 let h22 = ihashcall("JA1ABC", 22);
162 let h12 = ihashcall("JA1ABC", 12);
163 let h10 = ihashcall("JA1ABC", 10);
164 assert!(h22 < (1 << 22));
165 assert!(h12 < (1 << 12));
166 assert!(h10 < (1 << 10));
167 assert_eq!(h22, ihashcall("JA1ABC", 22));
169 }
170
171 #[test]
172 fn insert_and_lookup() {
173 let mut t = CallsignHashTable::new();
174 t.insert("JA1ABC");
175 t.insert("3Y0Z");
176
177 let h22 = ihashcall("JA1ABC", 22);
178 let h12 = ihashcall("JA1ABC", 12);
179 let h10 = ihashcall("JA1ABC", 10);
180
181 assert_eq!(t.lookup22(h22), Some("<JA1ABC>".to_string()));
182 assert_eq!(t.lookup12(h12), Some("JA1ABC"));
183 assert_eq!(t.lookup10(h10), Some("JA1ABC"));
184
185 let h22z = ihashcall("3Y0Z", 22);
186 assert_eq!(t.lookup22(h22z), Some("<3Y0Z>".to_string()));
187 }
188
189 #[test]
190 fn lru_eviction() {
191 let mut t = CallsignHashTable::new();
192 for i in 0..MAX_HASH22 + 10 {
194 t.insert(&format!("T{:04}X", i));
195 }
196 assert_eq!(t.len22(), MAX_HASH22);
197 }
198
199 #[test]
200 fn skip_special() {
201 let mut t = CallsignHashTable::new();
202 t.insert("<...>");
203 t.insert("CQ");
204 t.insert("CQ DX");
205 t.insert("");
206 t.insert("A"); assert_eq!(t.len22(), 0);
208 }
209
210 #[test]
211 fn strip_suffix() {
212 let mut t = CallsignHashTable::new();
213 t.insert("JA1ABC/P");
214 let h22 = ihashcall("JA1ABC", 22);
215 assert_eq!(t.lookup22(h22), Some("<JA1ABC>".to_string()));
216 }
217}