Accept messy input#

Schedule project data that strict validation rejects, such as data exported from dhtmlxGantt with duplicate ids or missing dates, and get a list of every fix the engine made.

By default a Project uses Validation::Strict and returns Error::Input for the first invalid field. Validation::Repair applies the same coercions dhtmlxGantt applies when it imports data, then schedules the result.

Steps#

  1. Create the project with repair validation:

    use plen_core::{Project, ProjectOptions, Validation};
    
    let mut project = Project::new(ProjectOptions {
        validation: Validation::Repair,
    });
    
  2. Pass your data to solve(Some(data), ...) as usual. Project::analyze with Some(data) repairs supplied data the same way. The one-shot schedule function always uses strict validation, so use a Project here.

  3. Read the Note::Repaired { path, message } entries in result.notes and show them to the user, or log them.

The full example below has four kinds of problems: a duplicate id, a task with no dates, a dated constraint with no date, and a task whose start, end and duration disagree. Strict validation rejects this data. Repair schedules it:

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

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

    // Start and end without a duration: valid, nothing to repair.
    let mut design = TaskData::new("design");
    design.start_date = Some(jan1);
    design.end_date = Some(jan4);

    // "build" appears twice. The later record wins.
    let mut build_old = TaskData::new("build");
    build_old.start_date = Some(jan1);
    build_old.duration = Some(1.0);
    let mut build = TaskData::new("build");
    build.start_date = Some(jan1);
    build.duration = Some(2.0);

    // No dates, no duration, and a dated constraint without `constraint_date`.
    let mut review = TaskData::new("review");
    review.constraint_type = Some(ConstraintType::Snet);

    // Three days between start and end, but `duration` says five.
    let mut deploy = TaskData::new("deploy");
    deploy.start_date = Some(jan1);
    deploy.end_date = Some(jan4);
    deploy.duration = Some(5.0);

    let mut data = ProjectData {
        tasks: vec![design, build_old, review, build, deploy],
        ..Default::default()
    };
    // 24/7 time keeps the example dates easy to check.
    data.config.use_calendars = false;

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

    for note in &result.notes {
        if let Note::Repaired { path, message } = note {
            println!("{path}: {message}");
        }
    }
    for m in &result.updates {
        if let (Some(start), Some(end), Some(duration)) = (m.start_date, m.end_date, m.duration) {
            println!("{} {}..{} {}", m.id, start, end, duration);
        }
    }
    Ok(())
}

What repair changes#

Repair reports each change below as one Note::Repaired, with the field path in path:

  • Duplicate ids in tasks, links, resources, assignments and calendars: the last record replaces the earlier one and takes its position. The note is at tasks[i].id, where i is the index of the duplicate in your input.
  • A task with no start and no end gets start_date set to config.project_start, or to the earliest date of any task if project_start isn't set.
  • A leaf task (not a milestone) with no duration and only one date gets duration: 1.
  • Start, end and duration that disagree: the dates win, and the duration becomes the working time between them. If the end is in non-working time, or the dates were computed on a different calendar, the duration is dropped and the task is resolved from its dates. An end that falls inside a pause moves to the pause start, with a second note at tasks[i].end_date.
  • A summary with only one date gets the other date computed from the date and its duration.
  • A dated constraint (Snet, Snlt, Fnet, Fnlt, Mso, Mfo) without constraint_date is cleared.
  • Invalid pauses on a task: the whole list is cleared, with a note at tasks[i].pauses.
  • An invalid assignment offset or duration is cleared, so the assignment books the whole task.

A task that already has two of start, end and duration is valid input. Repair doesn't touch it and doesn't report it.

Only the duplicate-id notes use input indexes. Every other note indexes the deduplicated list. In the example, the two build records become one, so deploy is tasks[4] in your input but tasks[3] in its note.

What repair doesn't fix#

  • An entirely undated project, with no config.project_start and no task that has a date, still fails with Error::Input at tasks[i].start_date. The engine never falls back to today's date.
  • Every other invalid input, such as a cycle in the parent hierarchy or a calendar with no working time, still returns Error::Input. See Error handling.
  • If repair or validation fails, solve returns Err and the project keeps its previous state.

Result#

The example prints the fixes, then the scheduled dates (the date lines may come in a different order):

tasks[3].id: duplicate id replaced the earlier record, retaining its position
tasks[2].constraint_type: dated constraint without date cleared
tasks[2].start_date: missing date defaulted to project anchor
tasks[2].duration: missing duration defaulted to one
tasks[3].duration: duration recomputed from supplied start and end
design 2026-01-01 00:00..2026-01-04 00:00 3
build 2026-01-01 00:00..2026-01-03 00:00 2
review 2026-01-01 00:00..2026-01-02 00:00 1
deploy 2026-01-01 00:00..2026-01-04 00:00 3

build uses the second record's duration of 2, and deploy has a duration of 3, taken from its dates. With Project::default(), the same solve call fails with Error::Input.