From 1514d25da1e7068820648e94df1970d688b872b4 Mon Sep 17 00:00:00 2001 From: yyc12345 Date: Wed, 22 Jul 2026 14:30:29 +0800 Subject: [PATCH] refactor: refactor kernel dataset into spec --- kernel/lcrconn-cli/src/app.rs | 4 +- kernel/lcrconn/src/dataset.rs | 457 -------------------------- kernel/lcrconn/src/lib.rs | 2 +- kernel/lcrconn/src/resolver/bfs.rs | 32 +- kernel/lcrconn/src/resolver/lut.rs | 12 +- kernel/lcrconn/src/spec.rs | 496 +++++++++++++++++++++++++++++ 6 files changed, 521 insertions(+), 482 deletions(-) delete mode 100644 kernel/lcrconn/src/dataset.rs create mode 100644 kernel/lcrconn/src/spec.rs diff --git a/kernel/lcrconn-cli/src/app.rs b/kernel/lcrconn-cli/src/app.rs index bce2cd9..4441f2e 100644 --- a/kernel/lcrconn-cli/src/app.rs +++ b/kernel/lcrconn-cli/src/app.rs @@ -3,7 +3,7 @@ use anyhow::Result; use lcrconn::{ BfsResolver, DeviceKind, LutResolver, Request, Resolver, Response, ResponsePriority, common::{Circuit, CircuitDeviceScale, JointKind, validate_device_value, validate_floating_point}, - dataset::{DatasetCollection, from_human_readable_value, to_human_readable_value}, + spec::{SpecCatalog, from_human_readable_value, to_human_readable_value}, query::MAX_RESPONSE_CNT, }; use std::io::Write; @@ -105,7 +105,7 @@ pub struct App { impl App { /// Create a new app with the given configuration. pub fn new(config: AppConfig) -> Result { - let datasets = DatasetCollection::from_file( + let datasets = SpecCatalog::from_file( config.get_resistor_dataset(), config.get_capacitor_dataset(), config.get_inductor_dataset(), diff --git a/kernel/lcrconn/src/dataset.rs b/kernel/lcrconn/src/dataset.rs deleted file mode 100644 index a131c13..0000000 --- a/kernel/lcrconn/src/dataset.rs +++ /dev/null @@ -1,457 +0,0 @@ -use crate::common::{ - DeviceValueError, FloatingPointError, validate_device_value, validate_floating_point, -}; -use ordered_float::OrderedFloat; -use std::collections::HashSet; -use std::fs::File; -use std::io::{BufRead, BufReader, BufWriter, Error as IoError, Write}; -use std::num::ParseFloatError; -use std::path::Path; -use thiserror::Error as TeError; - -/// Error occurs when building dataset. -#[derive(Debug, TeError)] -pub enum DatasetError { - #[error("invalid device value {0} in dataset item")] - BadDeviceValue(#[from] DeviceValueError), - #[error("unexpected empty string in dataset item")] - BlankDeviceValue, - #[error("bad string form of device value: {0}")] - ParseHumanReadableValue(#[from] ParseHumanReadableValueError), - #[error("duplicate item {0} in standard value list")] - DupDatasetItem(String), - #[error("unexpected empty standard value list")] - EmptyDataset, - #[error("fail to open dataset file: {0}")] - OpenDatasetFile(IoError), - #[error("fail to read dataset file: {0}")] - ReadDatasetFile(IoError), - #[error("fail to write dataset file: {0}")] - WriteDatasetFile(IoError), -} - -/// An item in the dataset. -#[derive(Debug, Clone)] -struct DatasetItem { - /// The actual value of this item. - value: f64, - /// The string form of this value given from original input for re-saving. - str_value: String, -} - -impl DatasetItem { - /// Create a new dataset item with validation. - fn new(value: f64, str_value: String) -> Result { - // Check arguments - let value = validate_device_value(value)?; - if str_value.is_empty() { - return Err(DatasetError::BlankDeviceValue); - } - Ok(Self { value, str_value }) - } -} - -/// A list holding available standard values for resistor, capacitor or inductor. -/// -/// Standard values is a collection of all possible values of specific device manufactured -/// by electronic factory. In reality, it also can be replaced by all possible values of -/// specific device provided by your laboratory. For example, your laboratory only provide -/// resistor with 100 Ohm and 4.7k Ohm. This list will only contain 100 and 4.7k. -pub struct Dataset { - /// A list of available device gauge values. - items: Vec, -} - -impl Dataset { - /// Internal used generic dataset creation function. - fn new(str_values: I) -> Result - where - I: IntoIterator, - { - // Check string form value one by one - let mut items: Vec = Vec::new(); - let mut seen: HashSet> = HashSet::new(); - - for str_value in str_values { - // Try parsing value - let value = from_human_readable_value(&str_value)?; - // Check and update set - if !seen.insert(OrderedFloat(value)) { - return Err(DatasetError::DupDatasetItem(str_value.to_string())); - } - // Add into result - items.push(DatasetItem::new(value, str_value)?); - } - - // Check empty case - if items.is_empty() { - return Err(DatasetError::EmptyDataset); - } - - // Ok, assign it - Ok(Self { items }) - } - - /// Create a dataset from an iterable of string values. - pub fn from_iterator(str_values: I) -> Result - where - I: IntoIterator, - S: Into, - { - Self::new(str_values.into_iter().map(|i| i.into())) - } - - /// Load a dataset from a block of text. - /// - /// Each non-empty line (after trimming whitespace) is treated as a value. - pub fn from_text(text: &str) -> Result { - let lines = text - .lines() - .map(|line| line.trim().to_string()) - .filter(|line| !line.is_empty()); - Self::from_iterator(lines) - } - - /// Load a dataset from a file. - /// - /// Each non-empty line (after trimming whitespace) is treated as a value. - pub fn from_file

(path: P) -> Result - where - P: AsRef, - { - let file = File::open(path).map_err(|err| DatasetError::OpenDatasetFile(err))?; - let reader = BufReader::new(file); - let lines = reader - .lines() - .map(|line| line.map(|line| line.trim().to_string())) - .filter(|line| !matches!(line, Ok(line) if line.is_empty())) - .collect::, _>>() - .map_err(|err| DatasetError::ReadDatasetFile(err))?; - Self::from_iterator(lines.into_iter()) - } - - /// The preset dataset for resistors. - pub fn resistor_preset() -> Result { - Self::from_iterator([ - "100", "220", "270", "390", "470", "680", "1k", "1.2k", "1.5k", "2.2k", "3.3k", "4.7k", - "6.8k", "10k", "47k", "100k", "1M", - ]) - } - - /// The preset dataset for capacitors. - pub fn capacitor_preset() -> Result { - Self::from_iterator([ - "10p", "22p", "33p", "47p", "68p", "100p", "150p", "220p", "330p", "470p", "560p", - "1u", "2.2u", "3.3u", "4.7u", "10u", "22u", "47u", "100u", "220u", "470u", - ]) - } - - /// The preset dataset for inductors. - pub fn inductor_preset() -> Result { - Self::from_iterator([ - "0.1u", "0.15u", "0.47u", "0.68u", "1u", "1.5u", "2.2u", "3.3u", "4.7u", "6.8u", - "8.2u", "10u", "15u", "22u", "33u", "47u", "68u", "100u", - ]) - } - - fn save(&self) -> impl Iterator { - self.items.iter().map(|i| i.str_value.as_str()) - } - - /// Get the string form of all values one by one for saving - pub fn save_iterator(&self) -> impl Iterator { - self.save() - } - - /// Get the string form of all values joined by newlines for saving./ - pub fn save_text(&self) -> String { - itertools::join(self.save_iterator(), "\n") - } - - /// Save all values joined by newlines to a file. - pub fn save_file

(&self, path: P) -> Result<(), DatasetError> - where - P: AsRef, - { - let file = File::open(path).map_err(|err| DatasetError::OpenDatasetFile(err))?; - let mut writer = BufWriter::new(file); - for line in self.save_iterator() { - writer - .write_all(line.as_bytes()) - .map_err(|err| DatasetError::WriteDatasetFile(err))?; - writer - .write_all("\n".as_bytes()) - .map_err(|err| DatasetError::WriteDatasetFile(err))?; - } - Ok(()) - } - - /// The number of available standard values. - pub fn len(&self) -> usize { - self.items.len() - } - - /// Get the available standard value by index. - pub fn get(&self, index: usize) -> Option { - self.items.get(index).map(|i| i.value) - } - - /// Get the available standard values as an iterator of `f64`. - pub fn values(&self) -> impl Iterator + Clone { - self.items.iter().map(|i| i.value) - } -} - -/// The collection holding all standard values for resistor, capacitor and inductor respectively. -pub struct DatasetCollection { - /// A list of available device gauge values for resistor. - resistor: Dataset, - /// A list of available device gauge values for capacitor. - capacitor: Dataset, - /// A list of available device gauge values for inductor. - inductor: Dataset, -} - -impl DatasetCollection { - /// Create dataset collection with 3 datasets for resistor, capacitor and inductor respectively. - pub fn new(resistor: Dataset, capacitor: Dataset, inductor: Dataset) -> Self { - Self { - resistor, - capacitor, - inductor, - } - } - - /// Load the standard values for resistor, capacitor and inductor respectively from iterables. - /// - /// * `resistor` - The iterable to load available standard values for resistor. - /// * `capacitor` - The iterable to load available standard values for capacitor. - /// * `inductor` - The iterable to load available standard values for inductor. - pub fn from_iterable( - resistor: I1, - capacitor: I2, - inductor: I3, - ) -> Result - where - I1: IntoIterator, - S1: Into, - I2: IntoIterator, - S2: Into, - I3: IntoIterator, - S3: Into, - { - Ok(Self { - resistor: Dataset::from_iterator(resistor)?, - capacitor: Dataset::from_iterator(capacitor)?, - inductor: Dataset::from_iterator(inductor)?, - }) - } - - /// Load the standard values from strings. - /// - /// * `resistor` - The string to load available standard values for resistor. - /// * `capacitor` - The string to load available standard values for capacitor. - /// * `inductor` - The string to load available standard values for inductor. - pub fn from_text( - resistor: &str, - capacitor: &str, - inductor: &str, - ) -> Result { - Ok(Self { - resistor: Dataset::from_text(resistor)?, - capacitor: Dataset::from_text(capacitor)?, - inductor: Dataset::from_text(inductor)?, - }) - } - - /// Load the standard values from files. - /// - /// * `resistor` - The file to load available standard values for resistor. - /// * `capacitor` - The file to load available standard values for capacitor. - /// * `inductor` - The file to load available standard values for inductor. - pub fn from_file( - resistor: P1, - capacitor: P2, - inductor: P3, - ) -> Result - where - P1: AsRef, - P2: AsRef, - P3: AsRef, - { - Ok(Self { - resistor: Dataset::from_file(resistor)?, - capacitor: Dataset::from_file(capacitor)?, - inductor: Dataset::from_file(inductor)?, - }) - } - - /// The preset dataset collection for all devices. - pub fn devices_preset() -> Result { - Ok(Self { - resistor: Dataset::resistor_preset()?, - capacitor: Dataset::capacitor_preset()?, - inductor: Dataset::inductor_preset()?, - }) - } - - /// Get the iterators for saving resistor, capacitor and inductor dataset respectively. - pub fn save_iterator( - &self, - ) -> ( - impl Iterator, - impl Iterator, - impl Iterator, - ) { - ( - self.resistor.save_iterator(), - self.capacitor.save_iterator(), - self.inductor.save_iterator(), - ) - } - - /// Get the string form of all values for saving resistor, capacitor and inductor dataset respectively. - pub fn save_text(&self) -> (String, String, String) { - ( - self.resistor.save_text(), - self.capacitor.save_text(), - self.inductor.save_text(), - ) - } - - /// Save all values to files. - /// - /// * `resistor` - The file to save available standard values for resistor. - /// * `capacitor` - The file to save available standard values for capacitor. - /// * `inductor` - The file to save available standard values for inductor. - pub fn save_file( - &self, - resistor: P1, - capacitor: P2, - inductor: P3, - ) -> Result<(), DatasetError> - where - P1: AsRef, - P2: AsRef, - P3: AsRef, - { - self.resistor.save_file(resistor)?; - self.capacitor.save_file(capacitor)?; - self.inductor.save_file(inductor)?; - Ok(()) - } - - /// Get the dataset for resistor. - pub fn resistor_dataset(&self) -> &Dataset { - &self.resistor - } - - /// Get the dataset for capacitor. - pub fn capacitor_dataset(&self) -> &Dataset { - &self.capacitor - } - - /// Get the dataset for inductor. - pub fn inductor_dataset(&self) -> &Dataset { - &self.inductor - } -} - -#[derive(Debug, TeError)] -pub enum ParseHumanReadableValueError { - #[error("fail to parse floating point part of given human readable value: {0}")] - ParseFloat(#[from] ParseFloatError), - #[error("arithmetic error: {0}")] - BadArithmetic(#[from] FloatingPointError), -} - -/// Convert human readable value to float. -/// -/// `strl` is the human readable value. -/// The return value is the parsed float value. -/// or error occurs when parsing. -pub fn from_human_readable_value(strl: &str) -> Result { - let strl = strl.trim(); - - let (num_part, multiplier) = if let Some(stripped) = strl.strip_suffix('n') { - (stripped, 1e-12) - } else if let Some(stripped) = strl.strip_suffix('p') { - (stripped, 1e-9) - } else if let Some(stripped) = strl.strip_suffix('u') { - (stripped, 1e-6) - } else if let Some(stripped) = strl.strip_suffix('m') { - (stripped, 1e-3) - } else if let Some(stripped) = strl.strip_suffix('k') { - (stripped, 1e3) - } else if let Some(stripped) = strl.strip_suffix('M') { - (stripped, 1e6) - } else if let Some(stripped) = strl.strip_suffix('G') { - (stripped, 1e9) - } else { - (strl, 1.0) - }; - - let num = num_part.parse::()?; - Ok(validate_floating_point(num * multiplier)?) -} - -/// The unit scale for human readable value. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum UnitScale { - NanoLower, - Nano, - Micro, - Milli, - None, - Kilo, - Mega, - Giga, - GigaHigher, -} - -/// Get the unit scale of human readable value. -/// -/// `v` is the value for analyzing scale. -pub fn get_human_readable_value_scale(v: f64) -> UnitScale { - let v = v.abs(); - if v < 1e-12 { - UnitScale::NanoLower - } else if v < 1e-9 { - UnitScale::Nano - } else if v < 1e-6 { - UnitScale::Micro - } else if v < 1e-3 { - UnitScale::Milli - } else if v < 1e3 { - UnitScale::None - } else if v < 1e6 { - UnitScale::Kilo - } else if v < 1e9 { - UnitScale::Mega - } else if v < 1e12 { - UnitScale::Giga - } else { - UnitScale::GigaHigher - } -} - -/// Convert float value to human readable value. -/// -/// `v`is the float value for formatting as human readable value. -pub fn to_human_readable_value(v: f64) -> String { - let scale = get_human_readable_value_scale(v); - match scale { - UnitScale::NanoLower => format!("{:+.4e} n", v / 1e-12), - UnitScale::Nano => format!("{:+.4} p", v / 1e-9), - UnitScale::Micro => format!("{:+.4} u", v / 1e-6), - UnitScale::Milli => format!("{:+.4} m", v / 1e-3), - // YYC MARK: - // The space of this format string is by design - // for keeping the same style with other format strings. - UnitScale::None => format!("{:+.4} ", v), - UnitScale::Kilo => format!("{:+.4} k", v / 1e3), - UnitScale::Mega => format!("{:+.4} M", v / 1e6), - UnitScale::Giga => format!("{:+.4} G", v / 1e9), - UnitScale::GigaHigher => format!("{:+.4e} G", v / 1e9), - } -} diff --git a/kernel/lcrconn/src/lib.rs b/kernel/lcrconn/src/lib.rs index 2a5dc56..fa7dae0 100644 --- a/kernel/lcrconn/src/lib.rs +++ b/kernel/lcrconn/src/lib.rs @@ -1,5 +1,5 @@ pub mod common; -pub mod dataset; +pub mod spec; pub mod query; pub mod resolver; diff --git a/kernel/lcrconn/src/resolver/bfs.rs b/kernel/lcrconn/src/resolver/bfs.rs index 20585f3..7adf9d6 100644 --- a/kernel/lcrconn/src/resolver/bfs.rs +++ b/kernel/lcrconn/src/resolver/bfs.rs @@ -2,7 +2,7 @@ use super::{Resolver, ResolverError}; use crate::common::{ Circuit, CircuitCalculator, CircuitCalculatorError, CircuitError, DeviceKind, JointKind, }; -use crate::dataset::{Dataset, DatasetCollection}; +use crate::spec::{SpecGroup, SpecCatalog}; use crate::query::{Request, Response, ResponseError}; use itertools::Itertools; use ordered_float::OrderedFloat; @@ -208,7 +208,7 @@ impl ResultBucket { /// A resolver that uses brute-force search to find the best matching circuits. pub struct BfsResolver { /// The datasets for all device kinds. - datasets: DatasetCollection, + datasets: SpecCatalog, } impl BfsResolver { @@ -222,21 +222,21 @@ impl BfsResolver { /// Iterate all possible circuits with one device without repeating equivalent topology. pub fn iter_one_device_circuit( - dataset: &Dataset, + dataset: &SpecGroup, ) -> impl Iterator> { // Every single device is unique so we directly output them. // This feature is insured by dataset itself. - dataset.values().map(|v1| Circuit::from_one_device(v1)) + dataset.specs().map(|v1| Circuit::from_one_device(v1)) } /// Iterate all possible circuits with two devices without repeating equivalent topology. pub fn iter_two_devices_circuit( - dataset: &Dataset, + dataset: &SpecGroup, ) -> impl Iterator> { // The two devices in this circuit is always swapable, // so we iterate them without repeating. itertools::iproduct!( - dataset.values().array_combinations_with_replacement::<2>(), + dataset.specs().array_combinations_with_replacement::<2>(), JointKind::iter() ) .map(|([v1, v2], j2)| Circuit::from_two_devices(v1, v2, j2)) @@ -244,7 +244,7 @@ impl BfsResolver { /// Iterate all possible circuits with three devices without repeating equivalent topology. pub fn iter_three_devices_circuit( - dataset: &Dataset, + dataset: &SpecGroup, ) -> impl Iterator> { // For generating three devices circuit, // it should be consisted by 2 parts. @@ -252,15 +252,15 @@ impl BfsResolver { // First, the whole circuit has only one joint type. // In this case, 3 devices are swapable and we should iterate them without repeating itertools::iproduct!( - dataset.values().array_combinations_with_replacement::<3>(), + dataset.specs().array_combinations_with_replacement::<3>(), JointKind::iter() ) .map(|([v1, v2, v3], j)| Circuit::from_three_devices(v1, v2, j, v3, j)), // Second, if the joint type is different, then the first 2 devices are swapable. // So we need iterate them without repeating. itertools::iproduct!( - dataset.values().array_combinations_with_replacement::<2>(), - dataset.values(), + dataset.specs().array_combinations_with_replacement::<2>(), + dataset.specs(), JointKind::iter() ) .map(|([v1, v2], v3, j)| Circuit::from_three_devices( @@ -276,20 +276,20 @@ impl BfsResolver { impl BfsResolver { /// Create a new BFS resolver with the given datasets. - pub fn new(datasets: DatasetCollection) -> Self { + pub fn new(datasets: SpecCatalog) -> Self { Self { datasets } } - fn pick_dataset(&self, device_kind: DeviceKind) -> &Dataset { + fn pick_dataset(&self, device_kind: DeviceKind) -> &SpecGroup { match device_kind { - DeviceKind::Resistor => self.datasets.resistor_dataset(), - DeviceKind::Capacitor => self.datasets.capacitor_dataset(), - DeviceKind::Inductor => self.datasets.inductor_dataset(), + DeviceKind::Resistor => self.datasets.resistor_specs(), + DeviceKind::Capacitor => self.datasets.capacitor_specs(), + DeviceKind::Inductor => self.datasets.inductor_specs(), } } fn bfs_iteration( - dataset: &Dataset, + dataset: &SpecGroup, ccalc: &CircuitCalculator, ) -> impl Iterator> { itertools::chain!( diff --git a/kernel/lcrconn/src/resolver/lut.rs b/kernel/lcrconn/src/resolver/lut.rs index 87aeaed..b683dbd 100644 --- a/kernel/lcrconn/src/resolver/lut.rs +++ b/kernel/lcrconn/src/resolver/lut.rs @@ -1,7 +1,7 @@ use super::bfs::BfsResolver; use super::{Resolver, ResolverError}; use crate::common::{Circuit, CircuitCalculator, CircuitCalculatorError, CircuitError, DeviceKind}; -use crate::dataset::{Dataset, DatasetCollection}; +use crate::spec::{SpecGroup, SpecCatalog}; use crate::query::{Request, Response, ResponseError}; use ordered_float::OrderedFloat; use thiserror::Error as TeError; @@ -127,16 +127,16 @@ pub struct LutResolver { impl LutResolver { /// Create a new LUT resolver by building lookup tables from the given datasets. - pub fn new(datasets: &DatasetCollection) -> Result { + pub fn new(datasets: &SpecCatalog) -> Result { Ok(Self { - resistor_lut: Self::build_lut(datasets.resistor_dataset(), DeviceKind::Resistor)?, - capacitor_lut: Self::build_lut(datasets.capacitor_dataset(), DeviceKind::Capacitor)?, - inductor_lut: Self::build_lut(datasets.inductor_dataset(), DeviceKind::Inductor)?, + resistor_lut: Self::build_lut(datasets.resistor_specs(), DeviceKind::Resistor)?, + capacitor_lut: Self::build_lut(datasets.capacitor_specs(), DeviceKind::Capacitor)?, + inductor_lut: Self::build_lut(datasets.inductor_specs(), DeviceKind::Inductor)?, }) } fn build_lut( - dataset: &Dataset, + dataset: &SpecGroup, device_kind: DeviceKind, ) -> Result, LutResolverError> { // Fetch all items diff --git a/kernel/lcrconn/src/spec.rs b/kernel/lcrconn/src/spec.rs new file mode 100644 index 0000000..a413ee3 --- /dev/null +++ b/kernel/lcrconn/src/spec.rs @@ -0,0 +1,496 @@ +//! Types for managing the rated values of components available in your lab. +//! +//! In this module, a "spec" means a single rated parameter value of a component, +//! such as `100` Ohms, `4.7k` Ohms, or `10u` Farads. It is **not** a general +//! technical specification document — it is simply the nominal value printed on +//! the component's body. +//! +//! - [`Spec`] — one rated value (e.g., 4.7k). +//! - [`SpecGroup`] — all rated values of a given component type that your lab +//! actually has in stock (e.g., all resistor values available in your drawer). +//! - [`SpecCatalog`] — the complete collection of rated values for resistors, +//! capacitors, and inductors. +//! +//! In short: these types answer the question "which exact component values can +//! I pick from the shelf?". + +use crate::common::{ + DeviceValueError, FloatingPointError, validate_device_value, validate_floating_point, +}; +use ordered_float::OrderedFloat; +use std::collections::HashSet; +use std::fs::File; +use std::io::{BufRead, BufReader, BufWriter, Error as IoError, Write}; +use std::num::ParseFloatError; +use std::path::Path; +use thiserror::Error as TeError; + +/// Errors that can occur when working with rated component values. +#[derive(Debug, TeError)] +pub enum SpecError { + #[error("invalid device value: {0}")] + BadDeviceValue(#[from] DeviceValueError), + #[error("bad string form of device value: {0}")] + ParseHumanReadableValue(#[from] ParseHumanReadableValueError), + #[error("duplicate rated value: {0}")] + DupSpecItem(String), + #[error("empty rated value group")] + EmptySpecGroup, + #[error("fail to open rated values file: {0}")] + OpenSpecFile(IoError), + #[error("fail to read rated values file: {0}")] + ReadSpecFile(IoError), + #[error("fail to write rated values file: {0}")] + WriteSpecFile(IoError), +} + +/// One rated value of a component (e.g., `4.7k` standing for 4700 Ohms). +/// +/// A `Spec` stores both the parsed numeric value and the original human-readable +/// string so that the value can be re-serialized exactly as it was entered. +#[derive(Debug, Clone)] +struct Spec { + /// The numeric rated value (e.g., `4700.0` for `"4.7k"`). + value: f64, + /// The original human-readable form (e.g., `"4.7k"`), kept for faithful + /// round-trip serialization. + str_value: String, +} + +impl Spec { + /// Create a new rated value from its human-readable representations. + pub fn new(str_value: String) -> Result { + // Try parsing value and check its range + let value = from_human_readable_value(&str_value)?; + let value = validate_device_value(value)?; + Ok(Self { value, str_value }) + } + + /// Get the numeric rated value (e.g., `4700.0` for `"4.7k"`). + pub fn get_value(&self) -> f64 { + self.value + } + + /// Get the original human-readable value form (e.g., `"4.7k"`). + pub fn get_str_value(&self) -> &str { + &self.str_value + } +} + +/// All rated values that your lab stocks for a single component type. +/// +/// For example, a `SpecGroup` for resistors might hold `{100, 220, 470, 1k, 4.7k, 10k}` +/// — these are the actual resistor values you have on hand. The same concept applies +/// to capacitors and inductors. +pub struct SpecGroup { + /// The rated values belonging to this group. + specs: Vec, +} + +impl SpecGroup { + /// Internal constructor: parse and deduplicate a sequence of human-readable rated values. + fn new(str_values: I) -> Result + where + I: IntoIterator, + { + // Check string form value one by one + let mut specs: Vec = Vec::new(); + let mut seen: HashSet> = HashSet::new(); + + for str_value in str_values { + // Build spec instance + let spec = Spec::new(str_value)?; + // Check and update set + if !seen.insert(OrderedFloat(spec.get_value())) { + return Err(SpecError::DupSpecItem(spec.get_str_value().to_string())); + } + // Add into result + specs.push(spec); + } + + // Check empty case + if specs.is_empty() { + return Err(SpecError::EmptySpecGroup); + } + + // Ok, assign it + Ok(Self { specs }) + } + + /// Build a spec group from any iterable of human-readable rated values (e.g., `"4.7k"`, `"100"`). + pub fn from_iterator(str_values: I) -> Result + where + I: IntoIterator, + S: Into, + { + Self::new(str_values.into_iter().map(|i| i.into())) + } + + /// Read rated values from a text block, one value per non-empty line. + pub fn from_text(text: &str) -> Result { + let lines = text + .lines() + .map(|line| line.trim().to_string()) + .filter(|line| !line.is_empty()); + Self::from_iterator(lines) + } + + /// Read rated values from a file, one value per non-empty line. + pub fn from_file

(path: P) -> Result + where + P: AsRef, + { + let file = File::open(path).map_err(|err| SpecError::OpenSpecFile(err))?; + let reader = BufReader::new(file); + let lines = reader + .lines() + .map(|line| line.map(|line| line.trim().to_string())) + .filter(|line| !matches!(line, Ok(line) if line.is_empty())) + .collect::, _>>() + .map_err(|err| SpecError::ReadSpecFile(err))?; + Self::from_iterator(lines.into_iter()) + } + + /// A commonly used set of resistor rated values (E12‑derived). + pub fn resistor_preset() -> Self { + Self::from_iterator([ + "100", "220", "270", "390", "470", "680", "1k", "1.2k", "1.5k", "2.2k", "3.3k", "4.7k", + "6.8k", "10k", "47k", "100k", "1M", + ]).expect("unexpected bad rated values set") + } + + /// A commonly used set of capacitor rated values. + pub fn capacitor_preset() -> Self { + Self::from_iterator([ + "10p", "22p", "33p", "47p", "68p", "100p", "150p", "220p", "330p", "470p", "560p", + "1u", "2.2u", "3.3u", "4.7u", "10u", "22u", "47u", "100u", "220u", "470u", + ]).expect("unexpected bad rated values set") + } + + /// A commonly used set of inductor rated values. + pub fn inductor_preset() -> Self { + Self::from_iterator([ + "0.1u", "0.15u", "0.47u", "0.68u", "1u", "1.5u", "2.2u", "3.3u", "4.7u", "6.8u", + "8.2u", "10u", "15u", "22u", "33u", "47u", "68u", "100u", + ]).expect("unexpected bad rated values set") + } + + fn save(&self) -> impl Iterator { + self.specs.iter().map(|i| i.str_value.as_str()) + } + + /// Iterate over the human-readable form of every rated value (for re-serialization). + pub fn save_iterator(&self) -> impl Iterator { + self.save() + } + + /// Join all rated values with newlines into a single string (for re-serialization). + pub fn save_text(&self) -> String { + itertools::join(self.save_iterator(), "\n") + } + + /// Write all rated values to a file, one per line. + pub fn save_file

(&self, path: P) -> Result<(), SpecError> + where + P: AsRef, + { + let file = File::open(path).map_err(|err| SpecError::OpenSpecFile(err))?; + let mut writer = BufWriter::new(file); + for line in self.save_iterator() { + writer + .write_all(line.as_bytes()) + .map_err(|err| SpecError::WriteSpecFile(err))?; + writer + .write_all("\n".as_bytes()) + .map_err(|err| SpecError::WriteSpecFile(err))?; + } + Ok(()) + } + + /// How many rated values this group contains. + pub fn len(&self) -> usize { + self.specs.len() + } + + /// Get the numeric rated value at the given index. + pub fn get(&self, index: usize) -> Option { + self.specs.get(index).map(|i| i.value) + } + + /// Iterate over all numeric rated values in this group. + pub fn specs(&self) -> impl Iterator + Clone { + self.specs.iter().map(|i| i.value) + } +} + +/// The full catalogue of rated component values your lab stocks. +/// +/// Bundles three [`SpecGroup`]s — one each for resistors, capacitors, and +/// inductors. This is the top-level entry point for answering "which component +/// values are available?". +pub struct SpecCatalog { + /// Rated values available for resistors. + resistor: SpecGroup, + /// Rated values available for capacitors. + capacitor: SpecGroup, + /// Rated values available for inductors. + inductor: SpecGroup, +} + +impl SpecCatalog { + /// Assemble a catalogue from the three device‑type spec groups. + pub fn new(resistor: SpecGroup, capacitor: SpecGroup, inductor: SpecGroup) -> Self { + Self { + resistor, + capacitor, + inductor, + } + } + + /// Build a catalogue from three iterables of human‑readable rated values. + /// + /// * `resistor` — values such as `"100"`, `"4.7k"`, etc. + /// * `capacitor` — values such as `"10p"`, `"4.7u"`, etc. + /// * `inductor` — values such as `"1u"`, `"10u"`, etc. + pub fn from_iterable( + resistor: I1, + capacitor: I2, + inductor: I3, + ) -> Result + where + I1: IntoIterator, + S1: Into, + I2: IntoIterator, + S2: Into, + I3: IntoIterator, + S3: Into, + { + Ok(Self { + resistor: SpecGroup::from_iterator(resistor)?, + capacitor: SpecGroup::from_iterator(capacitor)?, + inductor: SpecGroup::from_iterator(inductor)?, + }) + } + + /// Build a catalogue from three text blocks, one value per line. + /// + /// * `resistor` — the resistor rated‑values text. + /// * `capacitor` — the capacitor rated‑values text. + /// * `inductor` — the inductor rated‑values text. + pub fn from_text(resistor: &str, capacitor: &str, inductor: &str) -> Result { + Ok(Self { + resistor: SpecGroup::from_text(resistor)?, + capacitor: SpecGroup::from_text(capacitor)?, + inductor: SpecGroup::from_text(inductor)?, + }) + } + + /// Build a catalogue from three files, one value per line. + /// + /// * `resistor` — path to the resistor rated‑values file. + /// * `capacitor` — path to the capacitor rated‑values file. + /// * `inductor` — path to the inductor rated‑values file. + pub fn from_file( + resistor: P1, + capacitor: P2, + inductor: P3, + ) -> Result + where + P1: AsRef, + P2: AsRef, + P3: AsRef, + { + Ok(Self { + resistor: SpecGroup::from_file(resistor)?, + capacitor: SpecGroup::from_file(capacitor)?, + inductor: SpecGroup::from_file(inductor)?, + }) + } + + /// A ready‑to‑use catalogue with common resistor, capacitor and inductor rated values. + pub fn devices_preset() -> Self { + Self { + resistor: SpecGroup::resistor_preset(), + capacitor: SpecGroup::capacitor_preset(), + inductor: SpecGroup::inductor_preset(), + } + } + + /// Return one save‑iterator for each of the three device types. + pub fn save_iterator( + &self, + ) -> ( + impl Iterator, + impl Iterator, + impl Iterator, + ) { + ( + self.resistor.save_iterator(), + self.capacitor.save_iterator(), + self.inductor.save_iterator(), + ) + } + + /// Return the text representation of all three device‑type value sets. + pub fn save_text(&self) -> (String, String, String) { + ( + self.resistor.save_text(), + self.capacitor.save_text(), + self.inductor.save_text(), + ) + } + + /// Save all three device‑type value sets to files, one value per line. + /// + /// * `resistor` — file path for the resistor values. + /// * `capacitor` — file path for the capacitor values. + /// * `inductor` — file path for the inductor values. + pub fn save_file( + &self, + resistor: P1, + capacitor: P2, + inductor: P3, + ) -> Result<(), SpecError> + where + P1: AsRef, + P2: AsRef, + P3: AsRef, + { + self.resistor.save_file(resistor)?; + self.capacitor.save_file(capacitor)?; + self.inductor.save_file(inductor)?; + Ok(()) + } + + /// Access the resistor rated‑value set. + pub fn resistor_specs(&self) -> &SpecGroup { + &self.resistor + } + + /// Access the capacitor rated‑value set. + pub fn capacitor_specs(&self) -> &SpecGroup { + &self.capacitor + } + + /// Access the inductor rated‑value set. + pub fn inductor_specs(&self) -> &SpecGroup { + &self.inductor + } +} + +// region: Human Readable Value + +#[derive(Debug, TeError)] +pub enum ParseHumanReadableValueError { + #[error("fail to parse floating point part of given human readable value: {0}")] + ParseFloat(#[from] ParseFloatError), + #[error("arithmetic error: {0}")] + BadArithmetic(#[from] FloatingPointError), +} + +/// Convert human readable value to float. +/// +/// `strl` is the human readable value. +/// The return value is the parsed float value. or error occurs when parsing. +/// +/// This function guarantee that return value must be a valid floating value. +/// But do not guarantee that it can be used as device value. +/// It is possible that it is negative or zero floating point value. +pub fn from_human_readable_value(strl: &str) -> Result { + let strl = strl.trim(); + + let (num_part, multiplier) = if let Some(stripped) = strl.strip_suffix('n') { + (stripped, 1e-12) + } else if let Some(stripped) = strl.strip_suffix('p') { + (stripped, 1e-9) + } else if let Some(stripped) = strl.strip_suffix('u') { + (stripped, 1e-6) + } else if let Some(stripped) = strl.strip_suffix('m') { + (stripped, 1e-3) + } else if let Some(stripped) = strl.strip_suffix('k') { + (stripped, 1e3) + } else if let Some(stripped) = strl.strip_suffix('M') { + (stripped, 1e6) + } else if let Some(stripped) = strl.strip_suffix('G') { + (stripped, 1e9) + } else { + (strl, 1.0) + }; + + let num = num_part.parse::()?; + Ok(validate_floating_point(num * multiplier)?) +} + +/// The unit scale for human readable value. +#[derive(Debug, Clone, Copy)] +pub enum UnitScale { + NanoLower, + Nano, + Micro, + Milli, + None, + Kilo, + Mega, + Giga, + GigaHigher, +} + +/// Get the unit scale of human readable value. +/// +/// `v` is the value for analyzing scale. +/// It must be a valid floating point value. +/// +/// # Panics +/// +/// This function panics when given floating point value is bad. +pub fn get_human_readable_value_scale(v: f64) -> UnitScale { + let v = validate_floating_point(v).expect("unexpected bad floating point value"); + let v = v.abs(); + if v < 1e-12 { + UnitScale::NanoLower + } else if v < 1e-9 { + UnitScale::Nano + } else if v < 1e-6 { + UnitScale::Micro + } else if v < 1e-3 { + UnitScale::Milli + } else if v < 1e3 { + UnitScale::None + } else if v < 1e6 { + UnitScale::Kilo + } else if v < 1e9 { + UnitScale::Mega + } else if v < 1e12 { + UnitScale::Giga + } else { + UnitScale::GigaHigher + } +} + +/// Convert float value to human readable value. +/// +/// `v`is the float value for formatting as human readable value. +/// It must be a valid floating point value. +/// +/// # Panics +/// +/// This function panics when given floating point value is bad. +pub fn to_human_readable_value(v: f64) -> String { + let scale = get_human_readable_value_scale(v); + match scale { + UnitScale::NanoLower => format!("{:+.4e} n", v / 1e-12), + UnitScale::Nano => format!("{:+.4} p", v / 1e-9), + UnitScale::Micro => format!("{:+.4} u", v / 1e-6), + UnitScale::Milli => format!("{:+.4} m", v / 1e-3), + // YYC MARK: + // The space of this format string is by design + // for keeping the same style with other format strings. + UnitScale::None => format!("{:+.4} ", v), + UnitScale::Kilo => format!("{:+.4} k", v / 1e3), + UnitScale::Mega => format!("{:+.4} M", v / 1e6), + UnitScale::Giga => format!("{:+.4} G", v / 1e9), + UnitScale::GigaHigher => format!("{:+.4e} G", v / 1e9), + } +} + +// endregion