#![forbid(unsafe_code)]
use fp_expr::DataFrameExprExt;
use fp_frame::{DataFrame, Series};
use fp_join::DataFrameMergeExt;
use fp_types::{DType, Scalar};
fn sample_trades_csv() -> &'static str {
"date,ticker,price,volume\n\
2024-01-15,AAPL,185.50,1000\n\
2024-01-15,GOOG,140.25,500\n\
2024-01-16,AAPL,186.00,1200\n\
2024-01-16,GOOG,141.00,800\n\
2024-01-17,AAPL,184.75,900\n\
2024-01-17,GOOG,142.50,600\n\
2024-01-18,AAPL,187.25,1100\n\
2024-01-18,GOOG,143.00,700\n"
}
fn sample_sectors() -> DataFrame {
DataFrame::from_dict(
&["ticker", "sector"],
vec![
(
"ticker",
vec![
Scalar::Utf8("AAPL".into()),
Scalar::Utf8("GOOG".into()),
Scalar::Utf8("MSFT".into()),
],
),
(
"sector",
vec![
Scalar::Utf8("Technology".into()),
Scalar::Utf8("Technology".into()),
Scalar::Utf8("Technology".into()),
],
),
],
)
.unwrap()
}
#[test]
fn e2e_step1_csv_read() {
let csv = sample_trades_csv();
let frame = fp_io::read_csv_str(csv).expect("CSV read failed");
assert_eq!(frame.index().len(), 8, "expected 8 trade rows");
assert_eq!(frame.column_names().len(), 4, "expected 4 columns");
assert!(frame.column("date").is_some());
assert!(frame.column("ticker").is_some());
assert!(frame.column("price").is_some());
assert!(frame.column("volume").is_some());
}
#[test]
fn e2e_step1_csv_read_with_options() {
let csv = sample_trades_csv();
let opts = fp_io::CsvReadOptions {
usecols: Some(vec!["date".into(), "ticker".into(), "price".into()]),
nrows: Some(4),
..Default::default()
};
let frame = fp_io::read_csv_with_options(csv, &opts).expect("CSV read with options failed");
assert_eq!(frame.index().len(), 4, "nrows=4 should limit to 4 rows");
assert_eq!(
frame.column_names().len(),
3,
"usecols should select 3 columns"
);
assert!(frame.column("volume").is_none(), "volume not in usecols");
}
#[test]
fn e2e_step2_to_datetime() {
let csv = sample_trades_csv();
let frame = fp_io::read_csv_str(csv).unwrap();
let date_col_series = Series::new(
"date".to_owned(),
frame.index().clone(),
frame.column("date").unwrap().clone(),
)
.unwrap();
let parsed = fp_frame::to_datetime(&date_col_series).expect("to_datetime failed");
assert_eq!(parsed.len(), 8);
assert_eq!(
parsed.dtype(),
DType::datetime64_naive(),
"to_datetime yields datetime64[ns], matching pandas"
);
let first = &parsed.values()[0];
assert!(
matches!(first, Scalar::Datetime64(_)),
"expected typed Datetime64, got: {first:?}"
);
assert_eq!(
*first,
Scalar::Datetime64(1_705_276_800_000_000_000),
"expected 2024-01-15 00:00:00 (1705276800s * 1e9 nanos)"
);
}
#[test]
fn e2e_step3_query_filter() {
let csv = sample_trades_csv();
let frame = fp_io::read_csv_str(csv).unwrap();
let filtered = frame.query("price > 185").expect("query failed");
assert!(filtered.index().len() < 8, "filter should reduce rows");
for val in filtered.column("price").unwrap().values() {
if let Ok(v) = val.to_f64() {
assert!(v > 185.0, "filtered value {v} should be > 185");
}
}
}
#[test]
fn e2e_step4_groupby_agg() {
let csv = sample_trades_csv();
let frame = fp_io::read_csv_str(csv).unwrap();
let grouped = frame
.groupby(&["ticker"])
.expect("groupby failed")
.sum()
.expect("sum failed");
assert_eq!(grouped.index().len(), 2, "expected 2 groups");
let volume = grouped.column("volume").expect("volume column missing");
for val in volume.values() {
if let Ok(v) = val.to_f64() {
assert!(v > 0.0, "summed volume should be positive");
}
}
}
#[test]
fn e2e_step5_merge() {
let csv = sample_trades_csv();
let trades = fp_io::read_csv_str(csv).unwrap();
let sectors = sample_sectors();
let merged = trades
.merge(§ors, &["ticker"], fp_join::JoinType::Inner)
.expect("merge failed");
assert!(!merged.index.is_empty(), "merged should have rows");
assert!(
merged.columns.contains_key("sector"),
"sector column should be in merged result"
);
assert!(
merged.columns.contains_key("price"),
"price column should be in merged result"
);
}
#[test]
fn e2e_step6_sort() {
let csv = sample_trades_csv();
let frame = fp_io::read_csv_str(csv).unwrap();
let sorted = frame
.sort_values("price", false)
.expect("sort_values failed");
let prices = sorted.column("price").unwrap();
for i in 1..prices.len() {
let prev = prices.values()[i - 1].to_f64().unwrap_or(f64::NAN);
let curr = prices.values()[i].to_f64().unwrap_or(f64::NAN);
assert!(
prev >= curr,
"descending sort: {prev} should >= {curr} at index {i}"
);
}
}
#[test]
fn e2e_step7_export_csv_json() {
let csv = sample_trades_csv();
let frame = fp_io::read_csv_str(csv).unwrap();
let csv_out = fp_io::write_csv_string(&frame).expect("CSV write failed");
let csv_back = fp_io::read_csv_str(&csv_out).expect("CSV re-read failed");
assert_eq!(csv_back.index().len(), frame.index().len());
let json_out =
fp_io::write_json_string(&frame, fp_io::JsonOrient::Records).expect("JSON write failed");
let json_back =
fp_io::read_json_str(&json_out, fp_io::JsonOrient::Records).expect("JSON re-read failed");
assert_eq!(json_back.index().len(), frame.index().len());
}
#[test]
fn e2e_step7_export_feather() {
let csv = sample_trades_csv();
let frame = fp_io::read_csv_str(csv).unwrap();
let feather_bytes = fp_io::write_feather_bytes(&frame).expect("Feather write failed");
let feather_back = fp_io::read_feather_bytes(&feather_bytes).expect("Feather re-read failed");
assert_eq!(feather_back.index().len(), frame.index().len());
assert_eq!(
feather_back.column_names().len(),
frame.column_names().len()
);
}
#[test]
fn e2e_step7_export_sql() {
let csv = sample_trades_csv();
let frame = fp_io::read_csv_str(csv).unwrap();
let conn = rusqlite::Connection::open_in_memory().expect("sqlite open failed");
fp_io::write_sql(&frame, &conn, "trades", fp_io::SqlIfExists::Fail).expect("SQL write failed");
let sql_back = fp_io::read_sql_table(&conn, "trades").expect("SQL re-read failed");
assert_eq!(sql_back.index().len(), frame.index().len());
assert_eq!(
sql_back.column_names().len(),
frame.column_names().len() + 1
);
assert_eq!(sql_back.column_names()[0].as_str(), "index");
}
#[test]
fn e2e_full_pipeline() {
let trades = fp_io::read_csv_str(sample_trades_csv()).unwrap();
assert_eq!(trades.index().len(), 8);
let filtered = trades.query("price > 141").unwrap();
assert!(!filtered.is_empty());
let grouped = filtered.groupby(&["ticker"]).unwrap().sum().unwrap();
assert!(grouped.index().len() <= 2);
let sorted = grouped.sort_values("volume", false).unwrap();
assert_eq!(sorted.index().len(), grouped.index().len());
let feather = fp_io::write_feather_bytes(&sorted).unwrap();
assert!(!feather.is_empty());
let recovered = fp_io::read_feather_bytes(&feather).unwrap();
assert_eq!(recovered.index().len(), sorted.index().len());
let conn = rusqlite::Connection::open_in_memory().unwrap();
fp_io::write_sql(&recovered, &conn, "results", fp_io::SqlIfExists::Fail).unwrap();
let sql_result = fp_io::read_sql_table(&conn, "results").unwrap();
assert_eq!(sql_result.index().len(), recovered.index().len());
}