dugong 0.8.0

Dagre-compatible graph layout algorithms (port of dagrejs/dagre).
Documentation
use super::*;

#[derive(Debug)]
struct RecordingWorkControl {
    max: usize,
    used: usize,
    attempted: Vec<usize>,
}

impl RecordingWorkControl {
    fn unbounded() -> Self {
        Self {
            max: usize::MAX,
            used: 0,
            attempted: Vec::new(),
        }
    }
}

impl WorkControl for RecordingWorkControl {
    fn charge(&mut self, units: usize) -> Result<(), WorkError> {
        self.attempted.push(units);
        let next = self
            .used
            .checked_add(units)
            .ok_or(WorkError::ArithmeticOverflow)?;
        if next > self.max {
            return Err(WorkError::Interrupted);
        }
        self.used = next;
        Ok(())
    }
}

fn flat_layer(originals: &[usize]) -> LayerRank {
    let mut layer = LayerRank {
        nodes: vec![LayerNode::root()],
        local_by_original: HashMap::default(),
        root: 0,
        hierarchy_link_visits: 0,
        child_sort_work: 0,
    };
    for &original_ix in originals {
        let local_ix = layer.ensure_original(original_ix);
        layer.set_parent(local_ix, layer.root);
    }
    layer
}

fn barycenterless_entries(count: usize) -> Vec<BarycenterEntryIx> {
    (1..=count)
        .map(|v_ix| BarycenterEntryIx {
            v_ix,
            barycenter: None,
            weight: None,
        })
        .collect()
}

#[test]
fn conflict_replay_preserves_per_source_order_across_interleaved_inserts() {
    let layer = flat_layer(&[10, 20, 30, 40, 50]);
    let entries = barycenterless_entries(5);
    let mut constraints = ConstraintGraph::default();
    constraints.insert(20, 40);
    constraints.insert(10, 30);
    constraints.insert(20, 30);
    constraints.insert(10, 40);
    constraints.insert(30, 50);

    assert_eq!(
        constraints.outgoing.get(&10).map(Vec::as_slice),
        Some(&[30, 40][..])
    );
    assert_eq!(
        constraints.outgoing.get(&20).map(Vec::as_slice),
        Some(&[40, 30][..])
    );

    let mut scratch = SortScratch::default();
    let mut work = RecordingWorkControl::unbounded();
    let resolved =
        resolve_conflicts(&layer, &entries, &constraints, &mut scratch, 51, &mut work).unwrap();

    assert_eq!(work.attempted, vec![2, 2, 1]);
    assert_eq!(work.used, 5);
    assert_eq!(
        resolved,
        vec![
            SortEntryIx {
                vs: vec![2, 1, 4],
                i: 0,
                barycenter: None,
                weight: None,
            },
            SortEntryIx {
                vs: vec![3, 5],
                i: 2,
                barycenter: None,
                weight: None,
            },
        ]
    );
}

#[test]
fn conflict_replay_deduplicates_edges_and_charges_irrelevant_targets_once() {
    let layer = flat_layer(&[10, 20, 30]);
    let entries = barycenterless_entries(3);
    let mut constraints = ConstraintGraph::default();
    constraints.insert(10, 30);
    constraints.insert(10, 30);
    constraints.insert(10, 99);
    constraints.insert(99, 30);
    constraints.insert(20, 30);

    assert_eq!(
        constraints.outgoing.get(&10).map(Vec::as_slice),
        Some(&[30, 99][..])
    );

    let mut scratch = SortScratch::default();
    let mut limited = RecordingWorkControl {
        max: 2,
        used: 0,
        attempted: Vec::new(),
    };
    assert_eq!(
        resolve_conflicts(
            &layer,
            &entries,
            &constraints,
            &mut scratch,
            100,
            &mut limited,
        ),
        Err(WorkError::Interrupted)
    );
    assert_eq!(limited.attempted, vec![2, 1]);
    assert_eq!(limited.used, 2);

    let mut work = RecordingWorkControl::unbounded();
    let resolved =
        resolve_conflicts(&layer, &entries, &constraints, &mut scratch, 100, &mut work).unwrap();

    assert_eq!(work.attempted, vec![2, 1]);
    assert_eq!(work.used, 3);
    assert_eq!(
        resolved,
        vec![SortEntryIx {
            vs: vec![2, 1, 3],
            i: 0,
            barycenter: None,
            weight: None,
        }]
    );
}