numancy 1.0.2

Numerology calculations (Pythagorean and Brazilian cabalistic) as a pure, dependency-free Rust library.
Documentation
//! Signature / social-name analysis built on the cabalistic Life Triangle.
//!
//! A signature is analyzed like any name: its Expression number and its Life
//! Triangle (dominant arcana and regent). The per-arcane duration is estimated
//! by spreading the arcana over an assumed life span, so it is approximate.

use crate::cabalistic::expression_number;
use crate::error::NumerologyError;
use crate::pyramid::{life_triangle, ArcaneCode, LifeTriangle};
use crate::reduction::CalculatedNumber;

/// Default estimated life span (years) used to spread arcana over time.
pub const DEFAULT_LIFE_SPAN: u32 = 90;

/// The analysis of a candidate signature.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SignatureAnalysis {
  /// Expression number generated by the signature.
  pub expression: CalculatedNumber,
  /// Life Triangle of the signature.
  pub triangle: LifeTriangle,
  /// Dominant arcane at the requested age.
  pub dominant: Option<ArcaneCode>,
  /// Regent arcane (apex of the triangle).
  pub regent: u8,
}

impl SignatureAnalysis {
  /// Estimated number of years each arcane dominates.
  #[must_use]
  pub fn arcane_duration_years(&self, life_span: u32) -> f64 {
    if self.triangle.arcana.is_empty() {
      return 0.0;
    }
    f64::from(life_span) / self.triangle.arcana.len() as f64
  }
}

/// Analyze a signature at a given age, spreading arcana over `life_span`.
///
/// # Errors
/// Returns [`NumerologyError::EmptyName`] if the signature has fewer than two
/// usable letters.
pub fn analyze_signature(signature: &str, age: u32, life_span: u32) -> Result<SignatureAnalysis, NumerologyError> {
  let expression = expression_number(signature)?;
  let triangle = life_triangle(signature)?;
  let dominant = triangle.dominant(age, life_span);
  let regent = triangle.regent;
  Ok(SignatureAnalysis {
    expression,
    triangle,
    dominant,
    regent,
  })
}

#[cfg(test)]
mod tests {
  use super::*;

  #[test]
  fn analyzes_signature_like_a_name() {
    // Barbara Liskov: Expression 6, regent 7, 12 arcana over 90 years -> 7.5 each.
    let analysis = analyze_signature("Barbara Liskov", 55, DEFAULT_LIFE_SPAN).unwrap();
    assert_eq!(analysis.expression.value, 6);
    assert_eq!(analysis.regent, 7);
    assert_eq!(analysis.dominant.map(ArcaneCode::code), Some(31));
    assert!((analysis.arcane_duration_years(DEFAULT_LIFE_SPAN) - 7.5).abs() < 1e-9);
  }

  #[test]
  fn short_signature_errors() {
    assert!(analyze_signature("A", 30, DEFAULT_LIFE_SPAN).is_err());
  }
}