Validation#

Selects how a Project treats invalid ProjectData: reject it with an error, or coerce it into valid data the way dhtmlxGantt's import does. It's set through the validation field of ProjectOptions and applies to the data passed to Project::solve and Project::analyze.

Declaration#

#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Validation {
    #[default]
    Strict,
    Repair,
}

Variants#

Variant Description
Strict Default. Invalid input is rejected with Error::Input. The rules are listed on ProjectData.
Repair Fixes the input listed below before scheduling and reports each change as a Note::Repaired in the result's notes. Input it can't fix is still rejected with Error::Input.

What Repair changes#

Each row is one fix. The note's path points at the field that changed, indexed by its position in the input.

Input Fix Note path
Duplicate id in tasks, links, resources, assignments or calendars The last record wins and takes the position of the first one tasks[i].id, links[i].id, ...
Invalid pauses on a task The task's pause list is cleared tasks[i].pauses
Dated constraint without constraint_date constraint_type is cleared tasks[i].constraint_type
Task with neither start nor end start_date is set to config.project_start, or to the earliest date of any task when no project start is set tasks[i].start_date
Task (not a milestone) with no duration and only one of its dates duration is set to 1 tasks[i].duration
Start, end and duration that don't agree The dates win: the duration becomes the working time between them. When no duration from the start lands on the end (an end in non-working time, or dates computed on other calendars), the duration is dropped and the task is resolved from its dates tasks[i].duration
End date inside a work gap of a task with inconsistent dates The end moves to the start of the gap tasks[i].end_date
Summary with only one of its dates The missing date is computed from the other date and the duration tasks[i].start_date or tasks[i].end_date
Assignment offset that isn't finite or is negative offset is cleared assignments[i].offset
Assignment duration that isn't finite or isn't positive duration is cleared assignments[i].duration

Unscheduled tasks and summaries without dates are left without dates. Repair never falls back to the current date: when an undated task needs a start and the project has neither config.project_start nor any dated task, the call fails with Error::Input at tasks[i].start_date.

Example#

use plen_core::{
    DateTime, Note, Project, ProjectData, ProjectOptions, SolveOptions, TaskData, Validation,
};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let monday = DateTime::from_ymd_hm(2026, 1, 5, 8, 0).ok_or("invalid date")?;

    let mut design = TaskData::new("design");
    design.start_date = Some(monday);
    design.duration = Some(2.0);

    // Same id as the first task: this record replaces it.
    let mut duplicate = TaskData::new("design");
    duplicate.start_date = Some(monday);
    duplicate.duration = Some(3.0);

    // No dates: the start defaults to the earliest task date.
    let mut build = TaskData::new("build");
    build.duration = Some(2.0);

    let data = ProjectData {
        tasks: vec![design, duplicate, build],
        ..ProjectData::default()
    };

    let mut project = Project::new(ProjectOptions {
        validation: Validation::Repair,
    });
    let result = project.solve(Some(data), &SolveOptions::default())?;

    for note in &result.notes {
        if let Note::Repaired { path, message } = note {
            println!("{path}: {message}");
        }
    }
    Ok(())
}