1use std::collections::HashMap;
41use std::fs::File;
42use std::io::{BufReader, Read};
43use std::path::{Path, PathBuf};
44
45const MAGIC: &[u8; 4] = b"BC5D";
47
48const SUPPORTED_VERSION: u32 = 2;
50
51#[derive(Debug)]
53pub struct Bc5dTable {
54 caliber: f32,
56 data: Vec<f32>,
58 weight_bins: Vec<f32>,
60 bc_bins: Vec<f32>,
62 muzzle_vel_bins: Vec<f32>,
64 current_vel_bins: Vec<f32>,
66 num_drag_types: usize,
68 version: u32,
70 api_version: String,
72 timestamp: u64,
74}
75
76#[derive(Debug, Default)]
78pub struct Bc5dTableManager {
79 table_dir: Option<PathBuf>,
81 tables: HashMap<i32, Bc5dTable>,
83}
84
85#[derive(Debug)]
87pub enum Bc5dError {
88 IoError(std::io::Error),
89 InvalidMagic,
90 UnsupportedVersion(u32),
91 ChecksumMismatch { expected: u32, actual: u32 },
92 InvalidDimensions,
93 TableNotFound(f64),
94 NoTableDirectory,
95}
96
97impl std::fmt::Display for Bc5dError {
98 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
99 match self {
100 Bc5dError::IoError(e) => write!(f, "IO error: {}", e),
101 Bc5dError::InvalidMagic => write!(f, "Invalid file magic (expected 'BC5D')"),
102 Bc5dError::UnsupportedVersion(v) => write!(f, "Unsupported table version: {}", v),
103 Bc5dError::ChecksumMismatch { expected, actual } => {
104 write!(f, "Checksum mismatch: expected {:08x}, got {:08x}", expected, actual)
105 }
106 Bc5dError::InvalidDimensions => write!(f, "Invalid table dimensions"),
107 Bc5dError::TableNotFound(cal) => write!(f, "No BC5D table found for caliber {:.3}", cal),
108 Bc5dError::NoTableDirectory => write!(f, "No BC table directory configured"),
109 }
110 }
111}
112
113impl std::error::Error for Bc5dError {}
114
115impl From<std::io::Error> for Bc5dError {
116 fn from(e: std::io::Error) -> Self {
117 Bc5dError::IoError(e)
118 }
119}
120
121impl Bc5dTable {
122 pub fn load<P: AsRef<Path>>(path: P) -> Result<Self, Bc5dError> {
124 let file = File::open(&path)?;
125 Self::from_reader(BufReader::new(file))
126 }
127
128 pub fn from_bytes(bytes: &[u8]) -> Result<Self, Bc5dError> {
135 Self::from_reader(std::io::Cursor::new(bytes))
136 }
137
138 fn from_reader<R: Read>(mut reader: R) -> Result<Self, Bc5dError> {
139 let mut magic = [0u8; 4];
141 reader.read_exact(&mut magic)?;
142 if &magic != MAGIC {
143 return Err(Bc5dError::InvalidMagic);
144 }
145
146 let version = read_u32(&mut reader)?;
148 if version != SUPPORTED_VERSION {
149 return Err(Bc5dError::UnsupportedVersion(version));
150 }
151
152 let caliber = read_f32(&mut reader)?;
153 let _flags = read_u32(&mut reader)?;
154 let _padding = read_u32(&mut reader)?;
155
156 let dim_weight = read_u32(&mut reader)? as usize;
157 let dim_bc = read_u32(&mut reader)? as usize;
158 let dim_muzzle_vel = read_u32(&mut reader)? as usize;
159 let dim_current_vel = read_u32(&mut reader)? as usize;
160 let dim_drag_types = read_u32(&mut reader)? as usize;
161
162 let timestamp = read_u64(&mut reader)?;
163 let stored_checksum = read_u32(&mut reader)?;
164
165 let mut api_version_bytes = [0u8; 16];
167 reader.read_exact(&mut api_version_bytes)?;
168 let api_version = String::from_utf8_lossy(&api_version_bytes)
169 .trim_end_matches('\0')
170 .to_string();
171
172 let mut reserved = [0u8; 12];
174 reader.read_exact(&mut reserved)?;
175
176 if dim_weight == 0 || dim_bc == 0 || dim_muzzle_vel == 0 || dim_current_vel == 0 || dim_drag_types == 0 {
178 return Err(Bc5dError::InvalidDimensions);
179 }
180
181 let weight_bins = read_f32_array(&mut reader, dim_weight)?;
183 let bc_bins = read_f32_array(&mut reader, dim_bc)?;
184 let muzzle_vel_bins = read_f32_array(&mut reader, dim_muzzle_vel)?;
185 let current_vel_bins = read_f32_array(&mut reader, dim_current_vel)?;
186
187 const MAX_TOTAL_CELLS: usize = 64_000_000; let total_cells = dim_drag_types
192 .checked_mul(dim_weight)
193 .and_then(|x| x.checked_mul(dim_bc))
194 .and_then(|x| x.checked_mul(dim_muzzle_vel))
195 .and_then(|x| x.checked_mul(dim_current_vel))
196 .filter(|&n| n <= MAX_TOTAL_CELLS)
197 .ok_or(Bc5dError::InvalidDimensions)?;
198 let data = read_f32_array(&mut reader, total_cells)?;
199
200 let mut checksum_data = Vec::new();
202 for &v in &weight_bins {
203 checksum_data.extend_from_slice(&v.to_le_bytes());
204 }
205 for &v in &bc_bins {
206 checksum_data.extend_from_slice(&v.to_le_bytes());
207 }
208 for &v in &muzzle_vel_bins {
209 checksum_data.extend_from_slice(&v.to_le_bytes());
210 }
211 for &v in ¤t_vel_bins {
212 checksum_data.extend_from_slice(&v.to_le_bytes());
213 }
214 for &v in &data {
215 checksum_data.extend_from_slice(&v.to_le_bytes());
216 }
217
218 let calculated_checksum = crc32_ieee(&checksum_data);
219 if calculated_checksum != stored_checksum {
220 return Err(Bc5dError::ChecksumMismatch {
221 expected: stored_checksum,
222 actual: calculated_checksum,
223 });
224 }
225
226 Ok(Bc5dTable {
227 caliber,
228 data,
229 weight_bins,
230 bc_bins,
231 muzzle_vel_bins,
232 current_vel_bins,
233 num_drag_types: dim_drag_types,
234 version,
235 api_version,
236 timestamp,
237 })
238 }
239
240 pub fn lookup(
253 &self,
254 weight_grains: f64,
255 base_bc: f64,
256 muzzle_velocity: f64,
257 current_velocity: f64,
258 drag_type: &str,
259 ) -> f64 {
260 let drag_idx = if drag_type.eq_ignore_ascii_case("G7") { 1 } else { 0 };
262
263 let drag_idx = drag_idx.min(self.num_drag_types - 1);
265
266 let (weight_idx, weight_w) = self.interp_idx(weight_grains as f32, &self.weight_bins);
268 let (bc_idx, bc_w) = self.interp_idx(base_bc as f32, &self.bc_bins);
269 let (muzzle_idx, muzzle_w) = self.interp_idx(muzzle_velocity as f32, &self.muzzle_vel_bins);
270 let (current_idx, current_w) = self.interp_idx(current_velocity as f32, &self.current_vel_bins);
271
272 let mut result = 0.0f64;
274
275 for dw in 0..2 {
276 for db in 0..2 {
277 for dm in 0..2 {
278 for dc in 0..2 {
279 let weight = (if dw == 0 { 1.0 - weight_w } else { weight_w })
281 * (if db == 0 { 1.0 - bc_w } else { bc_w })
282 * (if dm == 0 { 1.0 - muzzle_w } else { muzzle_w })
283 * (if dc == 0 { 1.0 - current_w } else { current_w });
284
285 let wi = (weight_idx + dw).min(self.weight_bins.len() - 1);
287 let bi = (bc_idx + db).min(self.bc_bins.len() - 1);
288 let mi = (muzzle_idx + dm).min(self.muzzle_vel_bins.len() - 1);
289 let ci = (current_idx + dc).min(self.current_vel_bins.len() - 1);
290
291 let idx = self.flat_index(drag_idx, wi, bi, mi, ci);
293 result += weight * self.data[idx] as f64;
294 }
295 }
296 }
297 }
298
299 result.max(0.5).min(1.5)
301 }
302
303 pub fn get_effective_bc(
307 &self,
308 weight_grains: f64,
309 base_bc: f64,
310 muzzle_velocity: f64,
311 current_velocity: f64,
312 drag_type: &str,
313 ) -> f64 {
314 let correction = self.lookup(weight_grains, base_bc, muzzle_velocity, current_velocity, drag_type);
315 base_bc * correction
316 }
317
318 pub fn generate_segments(
333 &self,
334 base_bc: f64,
335 drag_type: &str,
336 weight_grains: f64,
337 muzzle_velocity_fps: Option<f64>,
338 ) -> Option<Vec<crate::BCSegmentData>> {
339 let mut breakpoints: Vec<f64> = vec![
340 4000.0, 3500.0, 3000.0, 2700.0, 2500.0, 2300.0, 2100.0, 2000.0, 1900.0, 1800.0, 1700.0,
341 1600.0, 1500.0, 1400.0, 1350.0, 1300.0, 1250.0, 1200.0, 1150.0, 1100.0, 1050.0, 1000.0,
342 950.0, 900.0, 850.0, 800.0, 700.0, 600.0, 500.0,
343 ];
344 if let Some(mv) = muzzle_velocity_fps {
345 breakpoints.push(mv);
346 }
347
348 let mut velocities: Vec<f64> = breakpoints
349 .into_iter()
350 .filter(|&v| v >= 500.0 && muzzle_velocity_fps.map_or(true, |mv| v <= mv))
351 .collect();
352 velocities.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal));
353 velocities.dedup();
354
355 let reference_mv = velocities.first().copied().unwrap_or(3000.0);
358
359 let mut segments: Vec<crate::BCSegmentData> = Vec::new();
360 let mut any_correction = false;
361 for i in 0..velocities.len().saturating_sub(1) {
362 let vel_max = velocities[i];
363 let vel_min = velocities[i + 1];
364 let vel_mid = (vel_max + vel_min) / 2.0;
365
366 let correction =
367 self.lookup(weight_grains, base_bc, reference_mv, vel_mid, drag_type);
368 if (correction - 1.0).abs() > 1e-6 {
369 any_correction = true;
370 }
371 segments.push(crate::BCSegmentData {
372 velocity_min: vel_min,
373 velocity_max: vel_max,
374 bc_value: base_bc * correction,
375 });
376 }
377
378 if any_correction && !segments.is_empty() {
379 Some(segments)
380 } else {
381 None
382 }
383 }
384
385 fn interp_idx(&self, value: f32, bins: &[f32]) -> (usize, f64) {
387 if bins.is_empty() {
388 return (0, 0.0);
389 }
390
391 if value <= bins[0] {
393 return (0, 0.0);
394 }
395 if value >= bins[bins.len() - 1] {
396 return (bins.len().saturating_sub(2), 1.0);
397 }
398
399 let idx = match bins.binary_search_by(|probe| {
401 probe.partial_cmp(&value).unwrap_or(std::cmp::Ordering::Equal)
402 }) {
403 Ok(i) => i.saturating_sub(1).min(bins.len() - 2),
404 Err(i) => i.saturating_sub(1).min(bins.len() - 2),
405 };
406
407 let low = bins[idx];
409 let high = bins[idx + 1];
410 let weight = if high > low {
411 ((value - low) / (high - low)) as f64
412 } else {
413 0.0
414 };
415
416 (idx, weight)
417 }
418
419 fn flat_index(&self, drag_idx: usize, weight_idx: usize, bc_idx: usize, muzzle_idx: usize, current_idx: usize) -> usize {
421 let n_weight = self.weight_bins.len();
422 let n_bc = self.bc_bins.len();
423 let n_muzzle = self.muzzle_vel_bins.len();
424 let n_current = self.current_vel_bins.len();
425
426 drag_idx * (n_weight * n_bc * n_muzzle * n_current)
427 + weight_idx * (n_bc * n_muzzle * n_current)
428 + bc_idx * (n_muzzle * n_current)
429 + muzzle_idx * n_current
430 + current_idx
431 }
432
433 pub fn caliber(&self) -> f32 {
435 self.caliber
436 }
437
438 pub fn version(&self) -> u32 {
440 self.version
441 }
442
443 pub fn api_version(&self) -> &str {
445 &self.api_version
446 }
447
448 pub fn timestamp(&self) -> u64 {
450 self.timestamp
451 }
452
453 pub fn total_cells(&self) -> usize {
455 self.data.len()
456 }
457
458 pub fn dimensions_str(&self) -> String {
460 format!(
461 "{}x{}x{}x{}x{} (weight x bc x muzzle_vel x current_vel x drag_types)",
462 self.weight_bins.len(),
463 self.bc_bins.len(),
464 self.muzzle_vel_bins.len(),
465 self.current_vel_bins.len(),
466 self.num_drag_types
467 )
468 }
469
470 pub fn weight_range(&self) -> (f32, f32) {
472 (*self.weight_bins.first().unwrap_or(&0.0), *self.weight_bins.last().unwrap_or(&0.0))
473 }
474
475 pub fn velocity_range(&self) -> (f32, f32) {
477 (*self.current_vel_bins.first().unwrap_or(&0.0), *self.current_vel_bins.last().unwrap_or(&0.0))
478 }
479}
480
481impl Bc5dTableManager {
482 pub fn new<P: AsRef<Path>>(table_dir: P) -> Self {
484 Bc5dTableManager {
485 table_dir: Some(table_dir.as_ref().to_path_buf()),
486 tables: HashMap::new(),
487 }
488 }
489
490 pub fn empty() -> Self {
492 Bc5dTableManager {
493 table_dir: None,
494 tables: HashMap::new(),
495 }
496 }
497
498 pub fn get_table(&mut self, caliber: f64) -> Result<&Bc5dTable, Bc5dError> {
502 let caliber_key = caliber_to_key(caliber);
503
504 if self.tables.contains_key(&caliber_key) {
506 return Ok(self.tables.get(&caliber_key).unwrap());
507 }
508
509 let table_dir = self.table_dir.as_ref().ok_or(Bc5dError::NoTableDirectory)?;
511 let table_path = find_table_file(table_dir, caliber)?;
512 let table = Bc5dTable::load(&table_path)?;
513 self.tables.insert(caliber_key, table);
514 Ok(self.tables.get(&caliber_key).unwrap())
515 }
516
517 pub fn lookup(
519 &mut self,
520 caliber: f64,
521 weight_grains: f64,
522 base_bc: f64,
523 muzzle_velocity: f64,
524 current_velocity: f64,
525 drag_type: &str,
526 ) -> Result<f64, Bc5dError> {
527 let table = self.get_table(caliber)?;
528 Ok(table.lookup(weight_grains, base_bc, muzzle_velocity, current_velocity, drag_type))
529 }
530
531 pub fn get_effective_bc(
533 &mut self,
534 caliber: f64,
535 weight_grains: f64,
536 base_bc: f64,
537 muzzle_velocity: f64,
538 current_velocity: f64,
539 drag_type: &str,
540 ) -> Result<f64, Bc5dError> {
541 let table = self.get_table(caliber)?;
542 Ok(table.get_effective_bc(weight_grains, base_bc, muzzle_velocity, current_velocity, drag_type))
543 }
544
545 pub fn has_table(&self, caliber: f64) -> bool {
547 if let Some(ref table_dir) = self.table_dir {
548 find_table_file(table_dir, caliber).is_ok()
549 } else {
550 false
551 }
552 }
553
554 pub fn available_calibers(&self) -> Vec<f64> {
556 let mut calibers = Vec::new();
557 if let Some(ref table_dir) = self.table_dir {
558 if let Ok(entries) = std::fs::read_dir(table_dir) {
559 for entry in entries.flatten() {
560 let path = entry.path();
561 if let Some(ext) = path.extension() {
562 if ext == "bin" {
563 if let Some(stem) = path.file_stem() {
564 let name = stem.to_string_lossy();
565 if name.starts_with("bc5d_") {
566 if let Ok(cal_int) = name[5..].parse::<i32>() {
568 calibers.push(cal_int as f64 / 1000.0);
569 }
570 }
571 }
572 }
573 }
574 }
575 }
576 }
577 calibers.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
578 calibers
579 }
580}
581
582fn caliber_to_key(caliber: f64) -> i32 {
584 (caliber * 1000.0).round() as i32
585}
586
587fn find_table_file(table_dir: &Path, caliber: f64) -> Result<PathBuf, Bc5dError> {
589 let caliber_int = (caliber * 1000.0).round() as i32;
590 let filename = format!("bc5d_{}.bin", caliber_int);
591 let path = table_dir.join(&filename);
592
593 if path.exists() {
594 return Ok(path);
595 }
596
597 let variations = [
599 format!("bc5d_{:03}.bin", caliber_int),
600 format!("bc5d_0{}.bin", caliber_int),
601 ];
602
603 for var in &variations {
604 let var_path = table_dir.join(var);
605 if var_path.exists() {
606 return Ok(var_path);
607 }
608 }
609
610 Err(Bc5dError::TableNotFound(caliber))
611}
612
613fn read_u32<R: Read>(reader: &mut R) -> Result<u32, std::io::Error> {
616 let mut buf = [0u8; 4];
617 reader.read_exact(&mut buf)?;
618 Ok(u32::from_le_bytes(buf))
619}
620
621fn read_u64<R: Read>(reader: &mut R) -> Result<u64, std::io::Error> {
622 let mut buf = [0u8; 8];
623 reader.read_exact(&mut buf)?;
624 Ok(u64::from_le_bytes(buf))
625}
626
627fn read_f32<R: Read>(reader: &mut R) -> Result<f32, std::io::Error> {
628 let mut buf = [0u8; 4];
629 reader.read_exact(&mut buf)?;
630 Ok(f32::from_le_bytes(buf))
631}
632
633fn read_f32_array<R: Read>(reader: &mut R, count: usize) -> Result<Vec<f32>, std::io::Error> {
634 const MAX_ELEMS: usize = 64_000_000; if count > MAX_ELEMS {
638 return Err(std::io::Error::new(
639 std::io::ErrorKind::InvalidData,
640 "f32 array length too large",
641 ));
642 }
643 let byte_len = count.checked_mul(4).ok_or_else(|| {
644 std::io::Error::new(std::io::ErrorKind::InvalidData, "f32 array length overflow")
645 })?;
646 let mut data = vec![0f32; count];
647 let mut buf = vec![0u8; byte_len];
648 reader.read_exact(&mut buf)?;
649
650 for (i, chunk) in buf.chunks_exact(4).enumerate() {
651 data[i] = f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
652 }
653
654 Ok(data)
655}
656
657pub(crate) fn crc32_ieee(data: &[u8]) -> u32 {
659 const TABLE: [u32; 256] = make_crc32_table();
660 let mut crc = 0xFFFFFFFFu32;
661 for &byte in data {
662 let idx = ((crc ^ byte as u32) & 0xFF) as usize;
663 crc = (crc >> 8) ^ TABLE[idx];
664 }
665 !crc
666}
667
668const fn make_crc32_table() -> [u32; 256] {
669 const POLY: u32 = 0xEDB88320;
670 let mut table = [0u32; 256];
671 let mut i = 0;
672 while i < 256 {
673 let mut crc = i as u32;
674 let mut j = 0;
675 while j < 8 {
676 if crc & 1 != 0 {
677 crc = (crc >> 1) ^ POLY;
678 } else {
679 crc >>= 1;
680 }
681 j += 1;
682 }
683 table[i] = crc;
684 i += 1;
685 }
686 table
687}
688
689#[cfg(test)]
690mod tests {
691 use super::*;
692
693 fn create_test_table() -> Bc5dTable {
694 let weight_bins = vec![100.0, 150.0, 200.0];
696 let bc_bins = vec![0.3, 0.4, 0.5];
697 let muzzle_vel_bins = vec![2500.0, 3000.0];
698 let current_vel_bins = vec![1000.0, 2000.0, 3000.0];
699 let num_drag_types = 2;
700
701 let total = num_drag_types * weight_bins.len() * bc_bins.len() * muzzle_vel_bins.len() * current_vel_bins.len();
703 let mut data = vec![1.0f32; total];
704
705 data[0] = 0.95; data[total - 1] = 1.05; Bc5dTable {
712 caliber: 0.308,
713 data,
714 weight_bins,
715 bc_bins,
716 muzzle_vel_bins,
717 current_vel_bins,
718 num_drag_types,
719 version: 2,
720 api_version: "test".to_string(),
721 timestamp: 0,
722 }
723 }
724
725 fn serialize_test_table(t: &Bc5dTable) -> Vec<u8> {
728 let mut out = Vec::new();
729 out.extend_from_slice(MAGIC);
730 out.extend_from_slice(&t.version.to_le_bytes());
731 out.extend_from_slice(&t.caliber.to_le_bytes());
732 out.extend_from_slice(&0u32.to_le_bytes()); out.extend_from_slice(&0u32.to_le_bytes()); out.extend_from_slice(&(t.weight_bins.len() as u32).to_le_bytes());
735 out.extend_from_slice(&(t.bc_bins.len() as u32).to_le_bytes());
736 out.extend_from_slice(&(t.muzzle_vel_bins.len() as u32).to_le_bytes());
737 out.extend_from_slice(&(t.current_vel_bins.len() as u32).to_le_bytes());
738 out.extend_from_slice(&(t.num_drag_types as u32).to_le_bytes());
739 out.extend_from_slice(&t.timestamp.to_le_bytes());
740
741 let mut checksum_data = Vec::new();
743 for v in t.weight_bins.iter().chain(&t.bc_bins).chain(&t.muzzle_vel_bins)
744 .chain(&t.current_vel_bins).chain(&t.data) {
745 checksum_data.extend_from_slice(&v.to_le_bytes());
746 }
747 out.extend_from_slice(&crc32_ieee(&checksum_data).to_le_bytes());
748
749 let mut api = [0u8; 16];
750 let bytes = t.api_version.as_bytes();
751 api[..bytes.len().min(16)].copy_from_slice(&bytes[..bytes.len().min(16)]);
752 out.extend_from_slice(&api);
753 out.extend_from_slice(&[0u8; 12]); for v in t.weight_bins.iter().chain(&t.bc_bins).chain(&t.muzzle_vel_bins)
756 .chain(&t.current_vel_bins).chain(&t.data) {
757 out.extend_from_slice(&v.to_le_bytes());
758 }
759 out
760 }
761
762 #[test]
763 fn test_from_bytes_roundtrip() {
764 let original = create_test_table();
765 let bytes = serialize_test_table(&original);
766 let parsed = Bc5dTable::from_bytes(&bytes).expect("from_bytes should parse");
767
768 assert_eq!(parsed.caliber, original.caliber);
769 assert_eq!(parsed.num_drag_types, original.num_drag_types);
770 assert_eq!(parsed.weight_bins, original.weight_bins);
771 assert_eq!(parsed.current_vel_bins, original.current_vel_bins);
772 assert_eq!(parsed.data, original.data);
773 assert_eq!(parsed.api_version, original.api_version);
774
775 let mut bad = bytes.clone();
777 *bad.last_mut().unwrap() ^= 0xFF;
778 assert!(Bc5dTable::from_bytes(&bad).is_err());
779 }
780
781 #[test]
782 fn test_generate_segments() {
783 let mut uniform = create_test_table();
786 uniform.data.iter_mut().for_each(|v| *v = 1.0);
787 assert!(uniform
788 .generate_segments(0.4, "G1", 150.0, Some(2700.0))
789 .is_none());
790
791 let mut corrected = create_test_table();
794 corrected.data.iter_mut().for_each(|v| *v = 0.9);
795 let segments = corrected
796 .generate_segments(0.4, "G1", 150.0, Some(2700.0))
797 .expect("segments expected for a table with corrections");
798 assert!(!segments.is_empty());
799 for w in segments.windows(2) {
800 assert!((segments[0].velocity_max - w[0].velocity_max).abs() >= 0.0);
802 assert!(w[0].velocity_min >= w[1].velocity_max - 1e-6);
803 }
804 for s in &segments {
805 assert!((s.bc_value - 0.4 * 0.9).abs() < 1e-6); assert!(s.velocity_max > s.velocity_min);
807 }
808 }
809
810 #[test]
811 fn test_interp_idx_in_range() {
812 let table = create_test_table();
813
814 let (idx, weight) = table.interp_idx(125.0, &table.weight_bins);
816 assert_eq!(idx, 0);
817 assert!((weight - 0.5).abs() < 0.01);
818
819 let (idx, weight) = table.interp_idx(150.0, &table.weight_bins);
821 assert_eq!(idx, 0);
822 assert!((weight - 1.0).abs() < 0.01);
823 }
824
825 #[test]
826 fn test_interp_idx_out_of_range() {
827 let table = create_test_table();
828
829 let (idx, weight) = table.interp_idx(50.0, &table.weight_bins);
831 assert_eq!(idx, 0);
832 assert_eq!(weight, 0.0);
833
834 let (idx, weight) = table.interp_idx(250.0, &table.weight_bins);
836 assert_eq!(idx, 1); assert_eq!(weight, 1.0);
838 }
839
840 #[test]
841 fn test_lookup_returns_valid_range() {
842 let table = create_test_table();
843
844 let correction = table.lookup(150.0, 0.4, 2750.0, 2000.0, "G1");
845 assert!(correction >= 0.5 && correction <= 1.5);
846
847 let correction = table.lookup(150.0, 0.4, 2750.0, 2000.0, "G7");
848 assert!(correction >= 0.5 && correction <= 1.5);
849 }
850
851 #[test]
852 fn test_effective_bc() {
853 let table = create_test_table();
854
855 let base_bc = 0.4;
856 let effective = table.get_effective_bc(150.0, base_bc, 2750.0, 2000.0, "G1");
857
858 assert!(effective >= base_bc * 0.5 && effective <= base_bc * 1.5);
860 }
861
862 #[test]
863 fn test_caliber_to_key() {
864 assert_eq!(caliber_to_key(0.308), 308);
865 assert_eq!(caliber_to_key(0.224), 224);
866 assert_eq!(caliber_to_key(0.338), 338);
867 }
868
869 #[test]
870 fn test_table_metadata() {
871 let table = create_test_table();
872 assert!((table.caliber() - 0.308).abs() < 0.001);
873 assert_eq!(table.version(), 2);
874 assert_eq!(table.api_version(), "test");
875 }
876
877 #[test]
878 fn test_crc32() {
879 let data = b"123456789";
881 let crc = crc32_ieee(data);
882 assert_eq!(crc, 0xCBF43926);
883 }
884}