use proc_macro2::TokenStream;
use quote::quote;
use syn::{Ident, Type};
use whippyunits_core::{
dimension_exponents::{DimensionBasis, DimensionExponents, DynDimensionExponents},
Dimension, SiPrefix, Unit,
};
use whippyunits_core::UnitExpr;
pub fn is_prefixed_base_unit(unit_name: &str) -> Option<(String, String)> {
if let Some((prefix, base)) = SiPrefix::strip_any_prefix_symbol(unit_name) {
if Dimension::find_unit_by_symbol(base).is_some() {
return Some((base.to_string(), prefix.symbol().to_string()));
}
}
if let Some((prefix, base)) = SiPrefix::strip_any_prefix_name(unit_name) {
if Dimension::find_unit_by_name(base).is_some() {
return Some((base.to_string(), prefix.symbol().to_string()));
}
}
None
}
fn lookup_si_prefix(prefix_symbol: &str) -> Option<&'static SiPrefix> {
SiPrefix::from_symbol(prefix_symbol)
}
pub fn scale_type_to_actual_unit_symbol(scale_type: &str) -> Option<String> {
match scale_type {
"Kilogram" => Some("kg".to_string()), "Millimeter" => Some("mm".to_string()), "Second" => Some("s".to_string()), "Ampere" => Some("A".to_string()), "Kelvin" => Some("K".to_string()), "Mole" => Some("mol".to_string()), "Candela" => Some("cd".to_string()), "Radian" => Some("rad".to_string()), _ => {
for unit in Unit::BASES.iter() {
if unit.name == scale_type {
return Some(unit.symbols[0].to_string());
}
}
for dimension in Dimension::ALL {
for unit in dimension.units {
if unit.name == scale_type {
return Some(unit.symbols[0].to_string());
}
}
}
None
}
}
}
pub struct UnitExprFormatter<F> {
pub unit_formatter: F,
}
impl<F> UnitExprFormatter<F>
where
F: Fn(&whippyunits_core::UnitExprUnit) -> String,
{
pub fn new(unit_formatter: F) -> Self {
Self { unit_formatter }
}
pub fn format(&self, expr: &UnitExpr) -> String {
match expr {
UnitExpr::Unit(unit) => (self.unit_formatter)(unit),
UnitExpr::Div(numerator, denominator) => {
let num_str = self.format(numerator);
let den_str = self.format(denominator);
format!("{} / {}", num_str, den_str)
}
UnitExpr::Mul(left, right) => {
let left_str = self.format(left);
let right_str = self.format(right);
format!("{} * {}", left_str, right_str)
}
UnitExpr::Pow(base, exponent) => {
let base_str = self.format(base);
format!("{}^{}", base_str, exponent)
}
}
}
}
pub struct DimensionProcessor {
pub dimensions: DynDimensionExponents,
}
impl DimensionProcessor {
pub fn new(dimensions: DynDimensionExponents) -> Self {
Self { dimensions }
}
pub fn apply_to_each<F>(&self, mut f: F)
where
F: FnMut(&str, i16),
{
let [mass_exp, length_exp, time_exp, current_exp, temp_exp, amount_exp, lum_exp, angle_exp] =
self.dimensions.0;
if mass_exp != 0 {
f("kg", mass_exp);
}
if length_exp != 0 {
f("m", length_exp);
}
if time_exp != 0 {
f("s", time_exp);
}
if current_exp != 0 {
f("A", current_exp);
}
if temp_exp != 0 {
f("K", temp_exp);
}
if amount_exp != 0 {
f("mol", amount_exp);
}
if lum_exp != 0 {
f("cd", lum_exp);
}
if angle_exp != 0 {
f("rad", angle_exp);
}
}
pub fn get_scale_identifier(
&self,
mass_scale: &Ident,
length_scale: &Ident,
time_scale: &Ident,
current_scale: &Ident,
temperature_scale: &Ident,
amount_scale: &Ident,
luminosity_scale: &Ident,
angle_scale: &Ident,
) -> Option<Ident> {
if let Some(basis) = self.dimensions.as_basis() {
let non_zero_count = self.dimensions.0.iter().filter(|&&x| x != 0).count();
if non_zero_count == 1 {
match basis {
DimensionBasis::Mass => Some(mass_scale.clone()),
DimensionBasis::Length => Some(length_scale.clone()),
DimensionBasis::Time => Some(time_scale.clone()),
DimensionBasis::Current => Some(current_scale.clone()),
DimensionBasis::Temperature => Some(temperature_scale.clone()),
DimensionBasis::Amount => Some(amount_scale.clone()),
DimensionBasis::Luminosity => Some(luminosity_scale.clone()),
DimensionBasis::Angle => Some(angle_scale.clone()),
}
} else {
None }
} else {
None }
}
}
pub struct QuoteGenerator<'a> {
pub storage_type: &'a Type,
pub brand_type: &'a Type,
pub lift_trace_doc_shadows: &'a TokenStream,
}
impl<'a> QuoteGenerator<'a> {
pub fn new(
storage_type: &'a Type,
brand_type: &'a Type,
lift_trace_doc_shadows: &'a TokenStream,
) -> Self {
Self {
storage_type,
brand_type,
lift_trace_doc_shadows,
}
}
pub fn generate_for_simple_base_unit(&self, scale_ident: &Ident) -> TokenStream {
let scale_name = scale_ident.to_string();
let unit_symbol = scale_type_to_actual_unit_symbol(&scale_name)
.unwrap_or_else(|| scale_name.to_lowercase());
let unit_symbol_ident =
syn::parse_str::<Ident>(&unit_symbol).unwrap_or_else(|_| scale_ident.clone());
let lift_trace_doc_shadows = self.lift_trace_doc_shadows;
let storage_type = self.storage_type;
let brand_type = self.brand_type;
quote! {
<whippyunits::Helper<{
#lift_trace_doc_shadows
0
}, whippyunits::unit!(#unit_symbol_ident, #storage_type, #brand_type)> as whippyunits::GetSecondGeneric>::Type
}
}
pub fn generate_for_compound_unit(&self, unit_expr_parsed: &syn::Expr) -> TokenStream {
let lift_trace_doc_shadows = self.lift_trace_doc_shadows;
let storage_type = self.storage_type;
let brand_type = self.brand_type;
quote! {
<whippyunits::Helper<{
#lift_trace_doc_shadows
0
}, whippyunits::unit!(#unit_expr_parsed, #storage_type, #brand_type)> as whippyunits::GetSecondGeneric>::Type
}
}
}
pub struct LocalContext {
pub mass_scale: Ident,
pub length_scale: Ident,
pub time_scale: Ident,
pub current_scale: Ident,
pub temperature_scale: Ident,
pub amount_scale: Ident,
pub luminosity_scale: Ident,
pub angle_scale: Ident,
}
pub fn is_prefixed_compound_unit(unit_symbol: &str) -> Option<(String, String)> {
if let Some((base_symbol, prefix)) = is_prefixed_base_unit(unit_symbol) {
if let Some((_unit, dimension)) = Dimension::find_unit_by_symbol(&base_symbol) {
let (m, l, t, c, temp, a, lum, ang) = (
dimension.exponents.0[0], dimension.exponents.0[1], dimension.exponents.0[2], dimension.exponents.0[3], dimension.exponents.0[4], dimension.exponents.0[5], dimension.exponents.0[6], dimension.exponents.0[7], );
let non_zero_count = [m, l, t, c, temp, a, lum, ang]
.iter()
.filter(|&&x| x != 0)
.count();
if non_zero_count > 1 {
return Some((base_symbol.to_string(), prefix.to_string()));
}
}
}
None
}
impl LocalContext {
pub fn unit_gets_transformed_in_local_context(&self, unit_name: &str) -> bool {
let (_unit, dimension) =
if let Some((unit, dimension)) = Dimension::find_unit_by_symbol(unit_name) {
(unit, dimension)
} else if let Some((base_symbol, _prefix)) = is_prefixed_base_unit(unit_name) {
if let Some((unit, dimension)) = Dimension::find_unit_by_symbol(&base_symbol) {
(unit, dimension)
} else {
return false;
}
} else if let Some((base_symbol, _prefix)) = is_prefixed_compound_unit(unit_name) {
if let Some((unit, dimension)) = Dimension::find_unit_by_symbol(&base_symbol) {
(unit, dimension)
} else {
return false;
}
} else {
return false;
};
let dimensions = dimension.exponents;
let processor = DimensionProcessor::new(dimensions);
if processor
.get_scale_identifier(
&self.mass_scale,
&self.length_scale,
&self.time_scale,
&self.current_scale,
&self.temperature_scale,
&self.amount_scale,
&self.luminosity_scale,
&self.angle_scale,
)
.is_some()
{
let scale_factor_diff = self.calculate_scale_factor_difference(dimensions);
return scale_factor_diff != 0;
}
return self.compound_unit_gets_transformed(dimensions);
}
pub fn compound_unit_gets_transformed(&self, dimensions: DynDimensionExponents) -> bool {
let processor = DimensionProcessor::new(dimensions);
let mut gets_transformed = false;
processor.apply_to_each(|unit_name, _exp| {
if self.unit_gets_transformed_in_local_context(unit_name) {
gets_transformed = true;
}
});
gets_transformed
}
pub fn calculate_scale_factor_difference(&self, dimensions: DynDimensionExponents) -> i16 {
let processor = DimensionProcessor::new(dimensions);
let mut total_scale_diff = 0;
processor.apply_to_each(|unit_name, exp| {
let scale_diff = self.get_scale_difference_for_base_unit(unit_name);
total_scale_diff += scale_diff * exp;
});
total_scale_diff
}
pub fn get_scale_difference_for_base_unit(&self, default_unit: &str) -> i16 {
let local_unit_symbol = match default_unit {
"kg" => scale_type_to_actual_unit_symbol(&self.mass_scale.to_string())
.unwrap_or_else(|| "kg".to_string()),
"m" => scale_type_to_actual_unit_symbol(&self.length_scale.to_string())
.unwrap_or_else(|| "m".to_string()),
"s" => scale_type_to_actual_unit_symbol(&self.time_scale.to_string())
.unwrap_or_else(|| "s".to_string()),
"A" => scale_type_to_actual_unit_symbol(&self.current_scale.to_string())
.unwrap_or_else(|| "A".to_string()),
"K" => scale_type_to_actual_unit_symbol(&self.temperature_scale.to_string())
.unwrap_or_else(|| "K".to_string()),
"mol" => scale_type_to_actual_unit_symbol(&self.amount_scale.to_string())
.unwrap_or_else(|| "mol".to_string()),
"cd" => scale_type_to_actual_unit_symbol(&self.luminosity_scale.to_string())
.unwrap_or_else(|| "cd".to_string()),
"rad" => scale_type_to_actual_unit_symbol(&self.angle_scale.to_string())
.unwrap_or_else(|| "rad".to_string()),
_ => default_unit.to_string(),
};
if local_unit_symbol == default_unit {
return 0;
}
if let Some((prefix_symbol, base_symbol)) = is_prefixed_base_unit(&local_unit_symbol) {
if base_symbol == default_unit {
if let Some(prefix_info) = lookup_si_prefix(&prefix_symbol) {
return prefix_info.factor_log10();
}
}
}
0
}
pub fn find_prefixed_type_by_scale_factor(
&self,
unit_name: &str,
scale_factor_diff: i16,
) -> Option<TokenStream> {
for prefix_info in SiPrefix::ALL {
if prefix_info.factor_log10() == scale_factor_diff {
let base_unit_name = self.get_base_unit_name(unit_name);
let prefixed_unit_name = format!("{}{}", prefix_info.symbol(), base_unit_name);
if let Some(declarator_type) =
crate::utils::shared_utils::get_declarator_type_for_unit(&prefixed_unit_name)
{
return Some(declarator_type);
}
}
}
None
}
}
impl LocalContext {
pub fn get_prefixed_unit_name(&self, unit_name: &str, scale_factor_diff: i16) -> String {
if let Some(prefix_info) = SiPrefix::ALL
.iter()
.find(|p| p.factor_log10() == scale_factor_diff)
{
let prefix_symbol = if prefix_info.symbol() == "u" {
"μ"
} else {
prefix_info.symbol()
};
let base_unit = if self.is_prefixed_unit(unit_name) {
self.get_base_unit_name(unit_name)
} else {
unit_name.to_string()
};
format!("{}{}", prefix_symbol, base_unit)
} else {
unit_name.to_string()
}
}
pub fn is_prefixed_unit(&self, unit_name: &str) -> bool {
if is_prefixed_base_unit(unit_name).is_some() {
return true;
}
if let Some((_base_symbol, _prefix)) = is_prefixed_compound_unit(unit_name) {
return true;
}
false
}
pub fn get_base_unit_name(&self, unit_name: &str) -> String {
if let Some((base_symbol, _prefix)) = is_prefixed_base_unit(unit_name) {
return base_symbol.to_string();
}
if let Some((base_symbol, _prefix)) = is_prefixed_compound_unit(unit_name) {
return base_symbol.to_string();
}
unit_name.to_string()
}
pub fn get_time_unit_conversion(&self, unit_name: &str) -> Option<String> {
if let Some((unit, dimension)) = Dimension::find_unit_by_symbol(unit_name) {
if unit.scale.0 != [0, 0, 0, 0] {
let storage_unit = dimension
.units
.iter()
.find(|u| {
u.scale == whippyunits_core::scale_exponents::ScaleExponents::IDENTITY
&& u.conversion_factor == 1.0
&& u.affine_offset == 0.0
})
.or_else(|| {
dimension.units.iter().find(|u| {
u.scale == whippyunits_core::scale_exponents::ScaleExponents::IDENTITY
&& u.conversion_factor == 1.0
})
});
if let Some(storage_unit) = storage_unit {
let storage_symbol = storage_unit.symbols[0];
let (p2, p3, p5, pi) = (
unit.scale.0[0],
unit.scale.0[1],
unit.scale.0[2],
unit.scale.0[3],
);
let conversion_factor = 2.0_f64.powi(p2 as i32)
* 3.0_f64.powi(p3 as i32)
* 5.0_f64.powi(p5 as i32)
* std::f64::consts::PI.powi(pi as i32);
if conversion_factor != 1.0 {
let factor_str = if conversion_factor.fract() == 0.0 {
format!("{}", conversion_factor as i64)
} else {
let formatted = format!("{:.3}", conversion_factor);
formatted
.trim_end_matches('0')
.trim_end_matches('.')
.to_string()
};
return Some(format!(
"{} → {}, factor: {}",
unit_name, storage_symbol, factor_str
));
}
}
}
}
None
}
}
pub struct TransformationDetails {
pub details: String,
}
impl TransformationDetails {
pub fn new(details: String) -> Self {
Self { details }
}
pub fn unknown_unit(unit_name: &str) -> Self {
Self::new(format!(" **{}**: Unknown unit", unit_name))
}
pub fn no_transformation(unit_name: &str, time_conversion: Option<String>) -> Self {
let mut details = format!("**{}**", unit_name);
if let Some(conversion) = time_conversion {
details.push_str(&format!(" = {}", conversion));
} else {
details.push_str(" (no change)");
}
Self::new(details)
}
}
impl LocalContext {
pub fn get_transformation_details_for_identifier(
&self,
unit_name: &str,
) -> TransformationDetails {
let (unit, dimension) = match self.find_unit_and_dimension(unit_name) {
Some(result) => result,
None => return TransformationDetails::unknown_unit(unit_name),
};
let effective_unit_name = if unit.affine_offset != 0.0 {
if let Some(storage_unit) = dimension.units.iter().find(|u| {
u.scale == unit.scale && u.conversion_factor == 1.0 && u.affine_offset == 0.0
}) {
storage_unit.symbols[0]
} else {
dimension
.units
.iter()
.find(|u| u.conversion_factor == 1.0 && u.affine_offset == 0.0)
.map(|u| u.symbols[0])
.unwrap_or(unit_name)
}
} else {
unit_name
};
let dimensions = dimension.exponents;
if self.unit_gets_transformed_in_local_context(effective_unit_name) {
let scale_factor_diff = self.calculate_scale_factor_difference(dimensions);
let details = self.generate_transformation_explanation(
effective_unit_name,
dimensions,
scale_factor_diff,
);
TransformationDetails::new(details)
} else {
let time_conversion = self.get_time_unit_conversion(effective_unit_name);
TransformationDetails::no_transformation(effective_unit_name, time_conversion)
}
}
fn find_unit_and_dimension(
&self,
unit_name: &str,
) -> Option<(&'static Unit, &'static Dimension)> {
if let Some((unit, dimension)) = Dimension::find_unit_by_symbol(unit_name) {
return Some((unit, dimension));
}
if let Some((base_symbol, _prefix)) = is_prefixed_base_unit(unit_name) {
return Dimension::find_unit_by_symbol(&base_symbol);
}
if let Some((base_symbol, _prefix)) = is_prefixed_compound_unit(unit_name) {
return Dimension::find_unit_by_symbol(&base_symbol);
}
None
}
pub fn generate_transformation_explanation(
&self,
unit_name: &str,
dimensions: DynDimensionExponents,
scale_factor_diff: i16,
) -> String {
let [mass_exp, length_exp, time_exp, current_exp, temp_exp, amount_exp, lum_exp, angle_exp] =
dimensions.0;
let is_prefixed_unit = self.is_prefixed_unit(unit_name);
let base_unit_name = if is_prefixed_unit {
self.get_base_unit_name(unit_name)
} else {
unit_name.to_string()
};
let mut dim_parts = Vec::new();
if mass_exp != 0 {
dim_parts.push(format!("kg^{}", mass_exp));
}
if length_exp != 0 {
dim_parts.push(format!("m^{}", length_exp));
}
if time_exp != 0 {
dim_parts.push(format!("s^{}", time_exp));
}
if current_exp != 0 {
dim_parts.push(format!("A^{}", current_exp));
}
if temp_exp != 0 {
dim_parts.push(format!("K^{}", temp_exp));
}
if amount_exp != 0 {
dim_parts.push(format!("mol^{}", amount_exp));
}
if lum_exp != 0 {
dim_parts.push(format!("cd^{}", lum_exp));
}
if angle_exp != 0 {
dim_parts.push(format!("rad^{}", angle_exp));
}
let original_dims = dim_parts.join(" * ");
let mut transformed_parts = Vec::new();
if mass_exp != 0 {
let mass_unit = if self.unit_gets_transformed_in_local_context("kg") {
"kg"
} else {
"kg"
};
transformed_parts.push(format!("{}^{}", mass_unit, mass_exp));
}
if length_exp != 0 {
let length_unit = if self.unit_gets_transformed_in_local_context("m") {
"mm"
} else {
"m"
};
transformed_parts.push(format!("{}^{}", length_unit, length_exp));
}
if time_exp != 0 {
let time_unit = if self.unit_gets_transformed_in_local_context("s") {
"s"
} else {
"s"
};
transformed_parts.push(format!("{}^{}", time_unit, time_exp));
}
if current_exp != 0 {
let current_unit = if self.unit_gets_transformed_in_local_context("A") {
"A"
} else {
"A"
};
transformed_parts.push(format!("{}^{}", current_unit, current_exp));
}
if temp_exp != 0 {
let temp_unit = if self.unit_gets_transformed_in_local_context("K") {
"K"
} else {
"K"
};
transformed_parts.push(format!("{}^{}", temp_unit, temp_exp));
}
if amount_exp != 0 {
let amount_unit = if self.unit_gets_transformed_in_local_context("mol") {
"mol"
} else {
"mol"
};
transformed_parts.push(format!("{}^{}", amount_unit, amount_exp));
}
if lum_exp != 0 {
let lum_unit = if self.unit_gets_transformed_in_local_context("cd") {
"cd"
} else {
"cd"
};
transformed_parts.push(format!("{}^{}", lum_unit, lum_exp));
}
if angle_exp != 0 {
let angle_unit = if self.unit_gets_transformed_in_local_context("rad") {
"rad"
} else {
"rad"
};
transformed_parts.push(format!("{}^{}", angle_unit, angle_exp));
}
let transformed_dims = transformed_parts.join(" * ");
let prefixed_unit_name = self.get_prefixed_unit_name(unit_name, scale_factor_diff);
let mut lines = Vec::new();
if is_prefixed_unit {
lines.push(format!(
"**{}** = {} (drop prefix: {} → {})",
base_unit_name, original_dims, unit_name, base_unit_name
));
} else {
lines.push(format!("**{}** = {}", unit_name, original_dims));
}
if scale_factor_diff != 0 {
if length_exp != 0 && self.unit_gets_transformed_in_local_context("m") {
lines.push(format!(" ↓ (length: m → mm, factor: 10^-3)"));
if length_exp != 1 {
lines.push(format!(
" ↓ (exponent: {}, total factor: 10^{})",
length_exp, scale_factor_diff
));
}
}
if mass_exp != 0 && self.unit_gets_transformed_in_local_context("kg") {
lines.push(format!(" ↓ (mass: kg → g, factor: 10^3)"));
if mass_exp != 1 {
lines.push(format!(
" ↓ (exponent: {}, total factor: 10^{})",
mass_exp, scale_factor_diff
));
}
}
}
if time_exp != 0 {
if let Some(time_conversion) = self.get_time_unit_conversion(unit_name) {
lines.push(format!(" ↓ ({})", time_conversion));
}
}
lines.push(format!(" = {}", transformed_dims));
lines.push(format!(" = **{}**", prefixed_unit_name));
lines.join("\n")
}
}