rayforce 1.0.0

Convenient, high-performance Rust bindings for RayforceDB v2
//! Phase 4: table construction, accessors, and CSV / splayed I/O.

use rayforce::{eval, Runtime, Table, Value};

fn sample_table() -> Table {
    // 3 rows: sym, price (f64), size (i64)
    let sym = Value::sym_vec(&["AAPL", "MSFT", "GOOG"]);
    let price = Value::vec(&[101.5f64, 202.0, 303.25]);
    let size = Value::vec(&[10i64, 20, 30]);
    Table::new(&["sym", "price", "size"], &[sym, price, size]).unwrap()
}

#[test]
fn construct_and_shape() {
    let _rt = Runtime::new().unwrap();
    let t = sample_table();
    assert_eq!(t.ncols(), 3);
    assert_eq!(t.nrows(), 3);
    assert_eq!(t.shape(), (3, 3));
    assert_eq!(t.column_names(), vec!["sym", "price", "size"]);
}

#[test]
fn column_access() {
    let _rt = Runtime::new().unwrap();
    let t = sample_table();

    let price = t.column("price").unwrap();
    assert_eq!(price.as_slice::<f64>().unwrap(), &[101.5, 202.0, 303.25]);

    let size = t.column_at(2).unwrap();
    assert_eq!(size.as_slice::<i64>().unwrap(), &[10, 20, 30]);

    let sym0 = t.column("sym").unwrap().get(0).unwrap().as_sym().unwrap();
    assert_eq!(sym0, "AAPL");

    assert!(t.column("nonexistent").is_err());
    assert!(t.column_at(9).is_err());
}

#[test]
fn all_columns() {
    let _rt = Runtime::new().unwrap();
    let t = sample_table();
    let cols = t.columns().unwrap();
    assert_eq!(cols.len(), 3);
    assert_eq!(cols[1].as_slice::<f64>().unwrap(), &[101.5, 202.0, 303.25]);
}

#[test]
fn value_table_interop() {
    let _rt = Runtime::new().unwrap();
    let t = sample_table();
    let v = t.clone().into_value();
    assert!(v.is_table());
    let back = v.as_table().unwrap();
    assert_eq!(back.shape(), (3, 3));

    // a non-table value cannot be made a Table
    assert!(Value::i64(5).as_table().is_err());
    assert!(Table::from_value(Value::i64(5)).is_err());
}

#[test]
fn matches_engine_table() {
    let _rt = Runtime::new().unwrap();
    // Build the same table the engine builds via a literal, compare formatting.
    let t = sample_table();
    let engine =
        eval("(table 'sym (list 'AAPL 'MSFT 'GOOG) 'price 101.5 202.0 303.25 'size 10 20 30)").ok();
    // The exact literal syntax may differ across engine versions; only assert
    // our table renders non-empty and has the expected shape-derived header.
    let rendered = format!("{t}");
    assert!(rendered.contains("sym") && rendered.contains("price") && rendered.contains("size"));
    let _ = engine; // engine literal is advisory; not asserted on
}

#[test]
fn csv_roundtrip() {
    let _rt = Runtime::new().unwrap();
    let t = sample_table();

    let dir = std::env::temp_dir();
    let path = dir.join(format!("rayforce_rs_csv_{}.csv", std::process::id()));
    let path_str = path.to_str().unwrap();

    t.write_csv(path_str).unwrap();
    assert!(path.exists());

    let loaded = Table::read_csv(&["SYMBOL", "F64", "I64"], path_str).unwrap();
    assert_eq!(loaded.ncols(), 3);
    assert_eq!(loaded.nrows(), 3);
    assert_eq!(
        loaded.column_at(1).unwrap().as_slice::<f64>().unwrap(),
        &[101.5, 202.0, 303.25]
    );

    let _ = std::fs::remove_file(&path);
}

#[test]
fn splayed_roundtrip() {
    let _rt = Runtime::new().unwrap();
    let t = sample_table();

    let base = std::env::temp_dir().join(format!("rayforce_rs_splay_{}", std::process::id()));
    let _ = std::fs::remove_dir_all(&base);
    let dir = base.join("t");
    std::fs::create_dir_all(&dir).unwrap();
    let dir_str = dir.to_str().unwrap();
    // Symbol columns require a symfile; supply an explicit one for both ends.
    let sym = base.join("sym");
    let sym_str = sym.to_str().unwrap();

    t.save_splayed(dir_str, Some(sym_str)).unwrap();
    let loaded = Table::load_splayed(dir_str, Some(sym_str)).unwrap();
    assert_eq!(loaded.shape(), (3, 3));
    assert_eq!(
        loaded.column("size").unwrap().as_slice::<i64>().unwrap(),
        &[10, 20, 30]
    );

    let _ = std::fs::remove_dir_all(&base);
}

#[test]
fn splayed_sym_values_roundtrip() {
    // A SYM column loaded from a splayed table must resolve to its original
    // values. The cells are positions in the column's *file* symbol domain;
    // resolving them against the runtime domain (the pre-fix behaviour) yields
    // garbage (e.g. "+"). `splayed_roundtrip` above only checks an i64 column,
    // so it never exercised symbol resolution.
    let _rt = Runtime::new().unwrap();
    let t = Table::new(
        &["k", "v"],
        &[
            Value::sym_vec(&["abcdef123456", "xyz"]),
            Value::vec(&[1i64, 2]),
        ],
    )
    .unwrap();

    let base = std::env::temp_dir().join(format!("rayforce_rs_splay_sym_{}", std::process::id()));
    let _ = std::fs::remove_dir_all(&base);
    let dir = base.join("t");
    std::fs::create_dir_all(&dir).unwrap();
    let dir_str = dir.to_str().unwrap();

    // <dir>/.sym dotfile convention (no explicit sym path).
    t.save_splayed(dir_str, None).unwrap();
    let loaded = Table::load_splayed(dir_str, None).unwrap();

    assert_eq!(loaded.shape(), (2, 2));
    let k = loaded.column("k").unwrap();
    assert_eq!(k.get(0).unwrap().as_sym().unwrap(), "abcdef123456");
    assert_eq!(k.get(1).unwrap().as_sym().unwrap(), "xyz");
    assert_eq!(
        loaded.column("v").unwrap().as_slice::<i64>().unwrap(),
        &[1, 2]
    );

    let _ = std::fs::remove_dir_all(&base);
}