Functional Weave
Code in TypeScript

construction.timber-length@1.0.0

impl/rust.rs

3,594 bytes · the Rust implementation · view raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

use super::funejson::Value;  ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one

/// Plan which pieces to cut from which stock length, first-fit decreasing.
///
/// A bar holds pieces p1..pn when their lengths plus a kerf between each pair
/// fit: sum + kerf * (n - 1) <= stock_length. The last piece may end exactly
/// at the end of the bar, which needs no final cut. Ignoring kerf is the usual
/// mistake: three 1200 mm pieces do not come out of one 3600 mm length.
///
/// # Panics
/// Panics on a stock length or cut that is not positive, a negative kerf, or
/// a cut longer than the stock length.
pub fn timber_cutting_list(cuts: &[i64], stock_length: i64, kerf: i64) -> CuttingList {
    if stock_length <= 0 {
        panic!("stockLength must be a whole number of millimetres greater than 0, received {}", stock_length);
    }
    if kerf < 0 {
        panic!("kerf must be a whole number of millimetres, 0 or more, received {}", kerf);
    }
    for (i, &cut) in cuts.iter().enumerate() {
        if cut <= 0 {
            panic!("cut {} must be a whole number of millimetres greater than 0, received {}", i + 1, cut);
        }
        if cut > stock_length {
            panic!("cut {} ({} mm) is longer than the stock length ({} mm)", i + 1, cut, stock_length);
        }
    }

    // Equal lengths are interchangeable, so a plain descending sort is deterministic.
    let mut sorted: Vec<i64> = cuts.to_vec();
    sorted.sort_by(|a, b| b.cmp(a));
    let mut pieces: Vec<Vec<i64>> = Vec::new();
    let mut used: Vec<i64> = Vec::new();
    for cut in sorted {
        let mut placed = false;
        for b in 0..pieces.len() {
            if used[b] + kerf + cut <= stock_length {
                pieces[b].push(cut);
                used[b] += kerf + cut;
                placed = true;
                break;
            }
        }
        if !placed {
            pieces.push(vec![cut]);
            used.push(cut);
        }
    }

    let total: i64 = cuts.iter().sum();
    let bars: Vec<CutBar> = pieces
        .into_iter()
        .enumerate()
        .map(|(b, p)| CutBar { cuts: p, offcut: (stock_length - used[b] - kerf).max(0) })
        .collect();
    let bar_count = bars.len() as i64;
    CuttingList { bars, bar_count, waste: bar_count * stock_length - total }
}

pub fn cutting_list_to_value(list: &CuttingList) -> Value {
    Value::obj(vec![
        (
            "bars",
            Value::Arr(
                list.bars
                    .iter()
                    .map(|bar| {
                        Value::obj(vec![
                            ("cuts", Value::Arr(bar.cuts.iter().map(|&c| Value::Int(c)).collect())),
                            ("offcut", Value::Int(bar.offcut)),
                        ])
                    })
                    .collect(),
            ),
        ),
        ("barCount", Value::Int(list.bar_count)),
        ("waste", Value::Int(list.waste)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    let mut cuts: Vec<i64> = Vec::new();
    for (i, v) in args[0].as_arr().iter().enumerate() {
        if let Value::Float(f) = v {
            panic!("cut {} must be a whole number of millimetres greater than 0, received {}", i + 1, f);
        }
        cuts.push(v.as_i64());
    }
    if let Value::Float(f) = &args[1] {
        panic!("stockLength must be a whole number of millimetres greater than 0, received {}", f);
    }
    if let Value::Float(f) = &args[2] {
        panic!("kerf must be a whole number of millimetres, 0 or more, received {}", f);
    }
    cutting_list_to_value(&timber_cutting_list(&cuts, args[1].as_i64(), args[2].as_i64()))
}