Merge pull request #322 from fiddlerwoaroof/feat/8jk6.3-date-hierarchy

feat(import): a date column imports as one hierarchical category (improvise-8jk6.3)
This commit is contained in:
Ed Langley
2026-10-02 22:32:35 -07:00
committed by GitHub
12 changed files with 919 additions and 20 deletions
+161
View File
@@ -665,6 +665,167 @@ fn a_headless_import_keeps_duplicates_separate_when_told_to() {
assert!(!doc.contains("= 30"), "{doc}");
}
/// Dates across two years, one per month, so nothing collides.
fn dated_csv(scratch: &Scratch) -> PathBuf {
let csv = scratch.path("dated.csv");
std::fs::write(
&csv,
"Date,Revenue\n2024-01-15,1\n2024-04-02,10\n2025-01-03,100\n",
)
.unwrap();
csv
}
/// improvise-8jk6.3, headless: `--extract` builds ONE hierarchical category by
/// default, as the wizard does, and it is written with its `>` lines.
#[test]
fn a_headless_date_import_is_one_hierarchical_category() {
let scratch = Scratch::new("date_hierarchy");
let out_path = scratch.path("out.improv");
let out = run_import(
&dated_csv(&scratch),
&out_path,
&[
"--extract",
"Date:Year",
"--extract",
"Date:Quarter",
"--extract",
"Date:Month",
],
);
assert!(out.status.success(), "{}", stderr_of(&out));
let doc = std::fs::read_to_string(&out_path).unwrap();
assert!(!doc.contains("Date_Month"), "{doc}");
assert!(doc.contains("- |2024-01|[|2024-Q1|]"), "{doc}");
assert!(doc.contains("> |2024-Q1|[|2024|]"), "{doc}");
assert!(!doc.contains("expanded"), "it opens folded: {doc}");
}
/// The flat split stays available headless, as `s` makes it in the wizard.
#[test]
fn a_headless_date_import_splits_when_told_to() {
let scratch = Scratch::new("date_split");
let out_path = scratch.path("out.improv");
let out = run_import(
&dated_csv(&scratch),
&out_path,
&["--extract", "Date:Month", "--split-date", "Date"],
);
assert!(out.status.success(), "{}", stderr_of(&out));
let doc = std::fs::read_to_string(&out_path).unwrap();
assert!(doc.contains("## Category: Date_Month"), "{doc}");
assert!(doc.contains("2024-01-15"), "the raw dates stay: {doc}");
}
/// improvise-8jk6.3 review: a `--split-date` that names nothing splittable
/// said nothing and exited 0.
#[test]
fn split_date_naming_an_unknown_or_non_date_field_is_refused() {
let scratch = Scratch::new("split_date_unknown");
for field in ["Nope", "Revenue"] {
let out_path = scratch.path("out.improv");
let out = run_import(
&dated_csv(&scratch),
&out_path,
&["--extract", "Date:Month", "--split-date", field],
);
assert!(!out.status.success(), "{field}: {}", stderr_of(&out));
let err = stderr_of(&out);
assert!(err.contains("--split-date") && err.contains(field), "{err}");
}
}
/// improvise-8jk6.3 review: `--axis Date_Month:column` names a category the
/// hierarchical import no longer builds, and was dropped without a word.
#[test]
fn an_axis_naming_a_category_the_import_did_not_build_is_refused() {
let scratch = Scratch::new("axis_unknown");
let out_path = scratch.path("out.improv");
let out = run_import(
&dated_csv(&scratch),
&out_path,
&["--extract", "Date:Month", "--axis", "Date_Month:column"],
);
assert!(!out.status.success(), "{}", stderr_of(&out));
let err = stderr_of(&out);
assert!(
err.contains("Date_Month") && err.contains("--split-date"),
"{err}"
);
}
/// `--time` on a column with no date format makes it a time category without
/// dates, so there is nothing to split.
#[test]
fn split_date_on_a_time_field_with_no_date_format_is_refused() {
let scratch = Scratch::new("split_date_no_format");
let out_path = scratch.path("out.improv");
let out = run_import(
&dated_csv(&scratch),
&out_path,
&["--time", "Revenue", "--split-date", "Revenue"],
);
assert!(!out.status.success(), "{}", stderr_of(&out));
assert!(
stderr_of(&out).contains("--split-date"),
"{}",
stderr_of(&out)
);
}
/// A name that is no date part gets the categories that exist, never the date
/// hint (the README's old `--axis Measure:column` was this).
#[test]
fn an_absent_axis_category_that_is_no_date_part_names_what_was_built() {
let scratch = Scratch::new("axis_unknown_plain");
let out_path = scratch.path("out.improv");
let out = run_import(
&dated_csv(&scratch),
&out_path,
&["--axis", "Measure:column"],
);
assert!(!out.status.success(), "{}", stderr_of(&out));
let err = stderr_of(&out);
assert!(
err.contains("'Measure'") && err.contains("_Measure"),
"{err}"
);
assert!(!err.contains("--split-date"), "no date hint: {err}");
}
/// The README's import recipe runs as printed.
#[test]
fn the_readmes_import_recipe_runs() {
let root = Path::new(env!("CARGO_MANIFEST_DIR"));
let readme = std::fs::read_to_string(root.join("README.md")).unwrap();
let start = readme.find("improvise import examples/demo.csv").unwrap();
let recipe = readme[start..]
.split("```")
.next()
.unwrap()
.replace("\\\n", " ");
let mut args: Vec<String> = vec![];
for (i, part) in recipe.split('"').enumerate() {
match i % 2 {
1 => args.push(part.to_string()),
_ => args.extend(part.split_whitespace().map(String::from)),
}
}
let at = args.iter().position(|a| a == "-o").unwrap();
args.truncate(at);
let scratch = Scratch::new("readme_recipe");
let out = Command::new(bin())
.arg("import")
.arg(root.join("examples/demo.csv"))
.args(args.iter().skip(3))
.arg("-o")
.arg(scratch.path("out.improv"))
.output()
.unwrap();
assert!(out.status.success(), "{}", stderr_of(&out));
}
/// code-maat's reports: three schemas, a stray note, and room for the output.
fn code_maat_folder(scratch: &Scratch) -> PathBuf {
let dir = scratch.path("maat");