use dpm_model::{DependencyKind, Priority, ProjectId, ThreePointEstimate, WorkItemId};
use uuid::Uuid;
const SECONDS_PER_HOUR: f64 = 3600.0;
const LAG_UNITS_PER_HOUR: f64 = 600.0;
pub(crate) const ELAPSED_HOURS_FORMAT: u32 = 6;
pub(crate) const DEFAULT_PRIORITY: i64 = 500;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum TimeBasis {
Working,
Elapsed,
}
pub(crate) fn time_basis(format: u32) -> Option<TimeBasis> {
let base = if (35..=44).contains(&format) {
format - 32
} else {
format
};
match base {
3 | 5 | 7 | 9 | 11 => Some(TimeBasis::Working),
4 | 6 | 8 | 10 | 12 => Some(TimeBasis::Elapsed),
_ => None,
}
}
pub(crate) fn duration_seconds(estimate: Option<ThreePointEstimate>) -> u64 {
estimate.map_or(0, |estimate| {
(estimate.pert_expected_hours() * SECONDS_PER_HOUR).round() as u64
})
}
pub(crate) fn estimate_from_seconds(seconds: u64) -> Option<ThreePointEstimate> {
(seconds > 0).then(|| {
let hours = seconds as f64 / SECONDS_PER_HOUR;
ThreePointEstimate {
optimistic_hours: hours,
likely_hours: hours,
pessimistic_hours: hours,
}
})
}
pub(crate) fn hours(seconds: u64) -> f64 {
seconds as f64 / SECONDS_PER_HOUR
}
pub(crate) fn lag_tenths(lag_hours: f64) -> i64 {
(lag_hours * LAG_UNITS_PER_HOUR).round() as i64
}
pub(crate) fn lag_hours(tenths: i64) -> f64 {
tenths as f64 / LAG_UNITS_PER_HOUR
}
pub(crate) fn format_duration(seconds: u64) -> String {
format!(
"PT{}H{}M{}S",
seconds / 3600,
seconds % 3600 / 60,
seconds % 60
)
}
pub(crate) fn parse_duration(text: &str) -> Result<u64, String> {
let body = text
.trim()
.strip_prefix("PT")
.ok_or_else(|| format!("duration {text} is not an hour/minute/second duration"))?;
let mut total = 0.0;
let mut number = String::new();
let mut last_rank = 0;
for character in body.chars() {
if character.is_ascii_digit() || character == '.' {
number.push(character);
continue;
}
let (rank, scale) = match character {
'H' => (1, SECONDS_PER_HOUR),
'M' => (2, 60.0),
'S' => (3, 1.0),
_ => {
return Err(format!(
"duration {text} has unsupported designator {character}"
));
}
};
let value: f64 = number
.parse()
.map_err(|_| format!("duration {text} has a malformed number"))?;
if rank <= last_rank {
return Err(format!("duration {text} repeats or reorders designators"));
}
total += value * scale;
last_rank = rank;
number.clear();
}
if !number.is_empty() || last_rank == 0 || !total.is_finite() {
return Err(format!("duration {text} is incomplete"));
}
Ok(total.round() as u64)
}
pub(crate) fn priority_value(priority: Priority) -> i64 {
match priority {
Priority::P0 => 900,
Priority::P1 => 700,
Priority::P2 => 500,
Priority::P3 => 300,
Priority::P4 => 100,
}
}
pub(crate) fn priority_from_value(value: i64) -> Priority {
match value {
800.. => Priority::P0,
600..=799 => Priority::P1,
400..=599 => Priority::P2,
200..=399 => Priority::P3,
_ => Priority::P4,
}
}
pub(crate) fn relation_code(kind: DependencyKind) -> u32 {
match kind {
DependencyKind::FinishFinish => 0,
DependencyKind::FinishStart => 1,
DependencyKind::StartFinish => 2,
DependencyKind::StartStart => 3,
}
}
pub(crate) fn relation_from_code(code: i64) -> Option<DependencyKind> {
match code {
0 => Some(DependencyKind::FinishFinish),
1 => Some(DependencyKind::FinishStart),
2 => Some(DependencyKind::StartFinish),
3 => Some(DependencyKind::StartStart),
_ => None,
}
}
pub(crate) fn format_guid(id: Uuid) -> String {
id.hyphenated().to_string().to_uppercase()
}
const FNV_OFFSET: u128 = 0x6c62_272e_07bb_0142_62b8_2175_6295_c58d;
const FNV_PRIME: u128 = 0x0000_0000_0100_0000_0000_0000_0000_013b;
const TASK_IDENTITY_DOMAIN: &[u8] = b"dpm.mspdi.task.v1\0";
const SCOPED_IDENTITY_DOMAIN: &[u8] = b"dpm.mspdi.task.scoped.v1\0";
fn avalanche(mut value: u64) -> u64 {
value ^= value >> 33;
value = value.wrapping_mul(0xff51_afd7_ed55_8ccd);
value ^= value >> 33;
value = value.wrapping_mul(0xc4ce_b9fe_1a85_ec53);
value ^ (value >> 33)
}
pub(crate) fn derived_work_id(project: Uuid, uid: i64) -> WorkItemId {
identity(&[TASK_IDENTITY_DOMAIN, project.as_bytes(), &uid.to_be_bytes()])
}
pub(crate) fn scoped_work_id(project: ProjectId, prefix: &str, uid: i64) -> WorkItemId {
let length = (prefix.len() as u64).to_be_bytes();
identity(&[
SCOPED_IDENTITY_DOMAIN,
project.0.as_bytes(),
&length,
prefix.as_bytes(),
&uid.to_be_bytes(),
])
}
fn identity(parts: &[&[u8]]) -> WorkItemId {
let mut hash = FNV_OFFSET;
for byte in parts.iter().flat_map(|part| part.iter()) {
hash ^= u128::from(*byte);
hash = hash.wrapping_mul(FNV_PRIME);
}
let (high, low) = ((hash >> 64) as u64, hash as u64);
let high = avalanche(high ^ low.rotate_left(29));
let low = avalanche(low ^ high);
let high = avalanche(high ^ low);
let mixed = (u128::from(high) << 64) | u128::from(low);
WorkItemId(uuid::Builder::from_custom_bytes(mixed.to_be_bytes()).into_uuid())
}
#[cfg(test)]
mod tests;