use super::funejson::Value; use super::dates_add_days::add_days; use super::dates_add_months::add_months; use super::dates_days_between::days_between; use super::math_round_div::round_div; fn whole_number(name: &str, value: i64) { if value < 0 { panic!("{} must be a whole number, 0 or more, received {}", name, value); } } /// The next service, by time or mileage, whichever comes first. /// /// "Whichever comes first" needs a date for the mileage limit too, so it is /// projected from the average daily mileage since the last service, rounded up /// to a whole day: a service booked for the projected day is never late on /// that estimate. /// /// # Panics /// Panics on a negative number, no interval at all, a malformed date, or an /// onDate or mileage before the last service. pub fn service_due( last_service_date: &str, last_service_mileage: i64, interval_months: i64, interval_miles: i64, on_date: &str, current_mileage: i64, ) -> ServiceDue { whole_number("lastServiceMileage", last_service_mileage); whole_number("intervalMonths", interval_months); whole_number("intervalMiles", interval_miles); whole_number("currentMileage", current_mileage); if interval_months == 0 && interval_miles == 0 { panic!("at least one of intervalMonths and intervalMiles must be above zero"); } let days_since = days_between(last_service_date, on_date); if days_since < 0 { panic!("onDate {} is before the last service on {}", on_date, last_service_date); } let miles_since = current_mileage - last_service_mileage; if miles_since < 0 { panic!( "currentMileage {} is below the mileage at the last service, {}", current_mileage, last_service_mileage ); } let due_date = if interval_months > 0 { Some(add_months(last_service_date, interval_months)) } else { None }; let days_remaining = due_date.as_ref().map(|d| days_between(on_date, d)); let due_mileage = if interval_miles > 0 { Some(last_service_mileage + interval_miles) } else { None }; let miles_remaining = due_mileage.map(|m| m - current_mileage); let mut projected: Option = None; let mut mileage_date: Option = None; if let Some(remaining) = miles_remaining { if remaining <= 0 { mileage_date = Some(on_date.to_string()); } else if days_since > 0 && miles_since > 0 { let date = add_days(on_date, round_div(remaining * days_since, miles_since, "up")); projected = Some(date.clone()); mileage_date = Some(date); } } let mut next_due = due_date.clone(); let mut due_by: Option = due_date.as_ref().map(|_| "date".to_string()); if let Some(md) = mileage_date { if next_due.as_ref().map_or(true, |n| md < *n) { next_due = Some(md); due_by = Some("mileage".to_string()); } } ServiceDue { overdue: days_remaining.map_or(false, |d| d < 0) || miles_remaining.map_or(false, |m| m < 0), due_date, due_mileage, days_remaining, miles_remaining, projected_mileage_date: projected, next_due_date: next_due, due_by, } } fn opt_str(v: &Option) -> Value { match v { Some(s) => Value::str(s), None => Value::Null, } } fn opt_int(v: Option) -> Value { match v { Some(i) => Value::Int(i), None => Value::Null, } } pub fn service_due_to_value(s: &ServiceDue) -> Value { Value::obj(vec![ ("dueDate", opt_str(&s.due_date)), ("dueMileage", opt_int(s.due_mileage)), ("daysRemaining", opt_int(s.days_remaining)), ("milesRemaining", opt_int(s.miles_remaining)), ("projectedMileageDate", opt_str(&s.projected_mileage_date)), ("nextDueDate", opt_str(&s.next_due_date)), ("dueBy", opt_str(&s.due_by)), ("overdue", Value::Bool(s.overdue)), ]) } pub fn fune_vector(args: &[Value]) -> Value { for (i, name) in [(1usize, "lastServiceMileage"), (2, "intervalMonths"), (3, "intervalMiles"), (5, "currentMileage")] { if let Value::Float(f) = &args[i] { panic!("{} must be a whole number, 0 or more, received {}", name, f); } } service_due_to_value(&service_due( args[0].as_str(), args[1].as_i64(), args[2].as_i64(), args[3].as_i64(), args[4].as_str(), args[5].as_i64(), )) }