use std::path::Path;
use tuitab::data::dataframe::DataFrame;
use tuitab::data::loader::load_csv;
fn load_sample() -> DataFrame {
load_csv(Path::new("test_data/sample.csv"), None).expect("Failed to load sample.csv")
}
#[test]
fn test_sort_integer_ascending() {
let mut df = load_sample();
df.sort_by(0, false); let values: Vec<String> = (0..df.visible_row_count())
.map(|i| DataFrame::anyvalue_to_string_fmt(&df.get_val(i, 0)))
.collect();
let ids: Vec<i64> = values.iter().map(|s| s.parse::<i64>().unwrap()).collect();
let mut expected = ids.clone();
expected.sort();
assert_eq!(ids, expected, "id column should be sorted ascending");
}
#[test]
fn test_sort_integer_descending() {
let mut df = load_sample();
df.sort_by(2, true); let first_age: i64 = DataFrame::anyvalue_to_string_fmt(&df.get_val(0, 2))
.parse::<i64>()
.unwrap();
let last_age: i64 =
DataFrame::anyvalue_to_string_fmt(&df.get_val(df.visible_row_count() - 1, 2))
.parse::<i64>()
.unwrap();
assert!(
first_age >= last_age,
"First age ({}) should be >= last age ({}) in descending sort",
first_age,
last_age
);
}
#[test]
fn test_sort_float() {
let mut df = load_sample();
df.sort_by(3, false); let values: Vec<f64> = (0..df.visible_row_count())
.map(|i| {
DataFrame::anyvalue_to_string_fmt(&df.get_val(i, 3))
.parse::<f64>()
.unwrap()
})
.collect();
for w in values.windows(2) {
assert!(w[0] <= w[1], "salary values should be non-decreasing");
}
}
#[test]
fn test_reset_sort() {
let mut df = load_sample();
let original_first = DataFrame::anyvalue_to_string_fmt(&df.get_val(0, 0));
df.sort_by(2, true); df.reset_sort();
assert_eq!(
DataFrame::anyvalue_to_string_fmt(&df.get_val(0, 0)),
original_first,
"After reset, first row should be back to original"
);
}
fn column_values(df: &DataFrame, col: usize) -> Vec<String> {
(0..df.visible_row_count())
.map(|i| DataFrame::anyvalue_to_string_fmt(&df.get_val(i, col)))
.collect()
}
#[test]
fn multi_key_sort_orders_by_the_second_key_within_the_first() {
let mut df = load_sample();
df.sort_by_keys(&[(4, false), (2, true)]).unwrap();
let departments = column_values(&df, 4);
let ages: Vec<i64> = column_values(&df, 2)
.iter()
.map(|s| s.parse().unwrap())
.collect();
let mut sorted_departments = departments.clone();
sorted_departments.sort();
assert_eq!(
departments, sorted_departments,
"primary key must be ordered"
);
for window in departments.windows(2).enumerate() {
let (i, pair) = window;
if pair[0] == pair[1] {
assert!(
ages[i] >= ages[i + 1],
"age must descend inside '{}': {} then {}",
pair[0],
ages[i],
ages[i + 1]
);
}
}
assert_eq!(&departments[0..7], &["Engineering"; 7]);
assert_eq!(&ages[0..7], &[52, 42, 37, 34, 33, 30, 28]);
}
#[test]
fn chaining_sorts_agrees_on_the_primary_key_but_promises_nothing_more() {
let mut chained = load_sample();
chained.sort_by(2, true); chained.sort_by(4, false);
let mut compound = load_sample();
compound.sort_by_keys(&[(4, false), (2, true)]).unwrap();
assert_eq!(
column_values(&chained, 4),
column_values(&compound, 4),
"the primary key is the only thing both forms are contracted to deliver"
);
}
#[test]
fn the_z_bracket_keys_build_the_same_compound_sort() {
use tuitab::types::Action;
let mut app = tuitab::app::App::new_as(Path::new("test_data/sample.csv"), None, None).unwrap();
app.stack.active_mut().cursor_col = 4;
app.handle_action(Action::SortAscending);
app.stack.active_mut().cursor_col = 2;
app.handle_action(Action::AddSortKeyDescending);
let sheet = app.stack.active();
assert_eq!(
sheet.sort_keys,
vec![("department".to_string(), false), ("age".to_string(), true)],
"both keys must be live, in the order they were pressed"
);
let mut expected = load_sample();
expected.sort_by_keys(&[(4, false), (2, true)]).unwrap();
for row in 0..expected.visible_row_count() {
for col in 0..expected.columns.len() {
assert_eq!(
DataFrame::anyvalue_to_string_fmt(&sheet.dataframe.get_val(row, col)),
DataFrame::anyvalue_to_string_fmt(&expected.get_val(row, col)),
"cell ({}, {})",
row,
col
);
}
}
}
#[test]
fn appending_a_column_already_sorted_replaces_its_direction() {
use tuitab::types::Action;
let mut app = tuitab::app::App::new_as(Path::new("test_data/sample.csv"), None, None).unwrap();
app.stack.active_mut().cursor_col = 4;
app.handle_action(Action::SortAscending);
app.stack.active_mut().cursor_col = 2;
app.handle_action(Action::AddSortKeyAscending);
assert_eq!(
app.stack.active().sort_keys,
vec![
("department".to_string(), false),
("age".to_string(), false)
]
);
app.handle_action(Action::AddSortKeyDescending);
assert_eq!(
app.stack.active().sort_keys,
vec![("department".to_string(), false), ("age".to_string(), true)],
"age flips direction; it does not appear twice"
);
}
#[test]
fn multi_key_sort_composes_with_a_prior_filter() {
let mut df = load_sample();
df.row_order = std::sync::Arc::new(vec![6, 9, 12, 16]);
df.sort_by_keys(&[(2, false)]).unwrap();
assert_eq!(df.visible_row_count(), 4, "sorting must not resurrect rows");
let ages: Vec<i64> = column_values(&df, 2)
.iter()
.map(|s| s.parse().unwrap())
.collect();
assert_eq!(ages, vec![25, 26, 27, 29]);
}
#[test]
fn a_sort_that_cannot_run_says_so() {
let mut df = load_sample();
assert!(
df.sort_by_keys(&[(99, false)]).is_err(),
"a column that does not exist is not a no-op"
);
assert!(df.sort_by_keys(&[]).is_err(), "no keys is not a sort");
assert!(df.sort_by_keys(&[(2, false)]).is_ok());
}