LevelingScope#

Limits a Project::level run to part of the plan: the resources whose overallocation gets resolved, the tasks the leveler may move, and the date range in which contention is acted on. Set it as LevelingOptions::scope. LevelingScope::default() sets every field to None, which is the same as leveling the whole project.

Declaration#

#[derive(Clone, Debug, Default)]
pub struct LevelingScope {
    pub resources: Option<Vec<Id>>,
    pub tasks: Option<Vec<Id>>,
    pub from: Option<DateTime>,
    pub to: Option<DateTime>,
}

Fields#

Field Type Default Description
resources Option<Vec<Id>> None (all resources) Resources whose overallocation the run resolves. Overallocation on any other resource is left as it is. Each id must name an existing resource.
tasks Option<Vec<Id>> None (all tasks) Tasks the leveler may delay or split. Tasks not in the list keep their dates. Each id must name an existing task.
from Option<DateTime> None (no lower bound) Start of the range, inclusive. A bucket is in scope when its start lies in [from, to).
to Option<DateTime> None (no upper bound) End of the range, exclusive.

The date range selects which overallocated buckets trigger a correction. It doesn't limit where tasks land or which tasks may move:

  • a delayed task can end up after to
  • a task that starts before from can still be delayed when it contends in a bucket inside the range
  • a window that straddles from or to is resolved only for its buckets inside the range

For how overallocation outside the scope is reported, see OverallocationWindow::reason.

See Project::level for the errors.

The scope also limits LevelingOptions::clear_first.

Example#

use plen_core::{
    AssignmentData, DateTime, LevelingOptions, LevelingScope, Project, ProjectData,
    ResourceData, SolveOptions, TaskData,
};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let day = |d| DateTime::from_ymd_hm(2026, 1, d, 0, 0).ok_or("invalid date");
    let assign = |task: &str| AssignmentData {
        id: None,
        task: task.into(),
        resource: "dev".into(),
        value: 1.0,
        offset: None,
        duration: None,
    };

    let mut a = TaskData::new("a");
    a.start_date = Some(day(5)?);
    a.duration = Some(3.0);
    let mut b = TaskData::new("b");
    b.start_date = Some(day(5)?);
    b.duration = Some(3.0);

    let data = ProjectData {
        tasks: vec![a, b],
        resources: vec![ResourceData { id: "dev".into(), capacity: None, calendar_id: None }],
        assignments: vec![assign("a"), assign("b")],
        ..Default::default()
    };

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

    // Resolve only overallocation of "dev" from Jan 5 on, and only by moving "b".
    let options = LevelingOptions {
        scope: Some(LevelingScope {
            resources: Some(vec!["dev".into()]),
            tasks: Some(vec!["b".into()]),
            from: Some(day(5)?),
            to: None,
        }),
        ..Default::default()
    };

    let result = project.level(&options)?;
    println!("rounds: {}", result.rounds);
    Ok(())
}