Project::apply#

Writes edits into the accepted state of a Project and marks it unsolved. It doesn't schedule anything: follow it with solve(None) or level to get the new dates.

Signature#

pub fn apply(&mut self, changes: &SchedulingChanges) -> Result<ApplyReport, Error>

Takes &mut self. Synchronous and CPU-only.

Parameters#

Name Type Description
changes &SchedulingChanges The edits to commit

Behavior#

  • Operations are ordered and partially accepted. A rejected operation goes into ApplyReport::rejected, not into an Err.
  • Any rejection sets needs_resync. After that, solve(None), apply and level fail until a full solve(Some(data)).
  • The state is unsolved afterwards, so preview, load_profile, evaluate, suggest_assignments and roster need a solve first.
  • Tasks added or updated by successful operations are reported by the next acyclic solve(None) or level.

Returns#

An ApplyReport with the applied and rejected operations and their side effects.

Errors#

Error When
Error::Project(ProjectError::NeedsResync) An earlier apply was rejected and no solve(Some(data)) followed (checked first)
Error::Project(ProjectError::StaleBaseline) changes.baseline_id doesn't name the current baseline
Note

A start date that falls on non-working time is kept unsnapped when the task has no predecessor. The end is computed forward from it on the working calendar.

Example#

Commit an edit, resync with full data when part of it is rejected, and reschedule.

use plen_core::{
    DateTime, Patch, Project, ProjectData, SchedulingChanges, SolveOptions, TaskData, TaskUpdate,
};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let monday = DateTime::from_ymd_hm(2026, 8, 10, 8, 0).ok_or("invalid date")?;
    let mut task = TaskData::new(1);
    task.start_date = Some(monday);
    task.duration = Some(2.0);
    let data = ProjectData {
        tasks: vec![task],
        ..Default::default()
    };

    let mut project = Project::default();
    let solved = project.solve(Some(data.clone()), &SolveOptions::default())?;

    let mut update = TaskUpdate::new(1);
    update.duration = Patch::Value(3.0);
    let mut changes = SchedulingChanges::default();
    changes.baseline_id = solved.baseline_id.clone();
    changes.tasks.update.push(update);

    let report = project.apply(&changes)?;
    for rejected in &report.rejected {
        println!("rejected at {}: {}", rejected.path, rejected.message);
    }
    let result = if report.needs_resync {
        project.solve(Some(data), &SolveOptions::default())?
    } else {
        project.solve(None, &SolveOptions::default())?
    };
    println!("{} task(s) moved", result.updates.len());
    Ok(())
}