plansolve 0.25.1

Official Rust client library for the PlanSolve optimization API.
Documentation
use std::fmt;
use std::sync::LazyLock;

use regex::Regex;

use crate::error::{Error, Result};

static SCORE_PATTERN: LazyLock<Regex> =
    LazyLock::new(|| Regex::new(r"^(-?\d+)hard/(-?\d+)medium/(-?\d+)soft$").unwrap());

/// An optimization score in the format `Xhard/Ymedium/Zsoft`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Score {
    pub hard: i64,
    pub medium: i64,
    pub soft: i64,
}

impl Score {
    /// Parses a score string in the format `Xhard/Ymedium/Zsoft`.
    pub fn parse(value: &str) -> Result<Score> {
        let caps = SCORE_PATTERN
            .captures(value)
            .ok_or_else(|| Error::InvalidScore(value.to_string()))?;

        // Groups are guaranteed to be valid integers by the regex.
        Ok(Score {
            hard: caps[1].parse().unwrap(),
            medium: caps[2].parse().unwrap(),
            soft: caps[3].parse().unwrap(),
        })
    }

    /// Returns true when the solution violates no hard constraints.
    pub fn is_feasible(&self) -> bool {
        self.hard >= 0
    }
}

impl fmt::Display for Score {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "{}hard/{}medium/{}soft",
            self.hard, self.medium, self.soft
        )
    }
}

impl std::str::FromStr for Score {
    type Err = Error;
    fn from_str(s: &str) -> Result<Self> {
        Score::parse(s)
    }
}

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

    #[test]
    fn parses_valid_score() {
        let s = Score::parse("-1hard/0medium/25soft").unwrap();
        assert_eq!(
            s,
            Score {
                hard: -1,
                medium: 0,
                soft: 25
            }
        );
        assert_eq!(s.to_string(), "-1hard/0medium/25soft");
        assert!(!s.is_feasible());
    }

    #[test]
    fn rejects_garbage() {
        assert!(Score::parse("not-a-score").is_err());
        assert!(Score::parse("").is_err());
    }
}