mirror of
https://github.com/storytold/wordcraft.git
synced 2026-10-28 12:27:11 -06:00
Merge PR #4226: Fields: \* DollarText and \* Hex write the result, and PAGE reads \* ArabicDash
This commit is contained in:
@@ -456,6 +456,7 @@ Imported Word 97–2003 `.doc`/`.dot` files now ask for a supported DOCX target
|
||||
| 2026-10-12 | STYLEREF fields (#775): running headers and footers show each page's chapter title (page's first, or `\l` last, paragraph of the style, else the nearest before), style names, ids, heading levels and character styles, number switches; body STYLEREF updates with the other fields; STYLEREF in the Field dialog; PDF export follows the layout |
|
||||
| 2026-10-24 | Document information fields: SUBJECT, KEYWORDS, COMMENTS, LASTSAVEDBY, REVNUM and NUMCHARS update with the other fields and are listed in the Field dialog; a quoted text after TITLE, SUBJECT, AUTHOR, KEYWORDS or COMMENTS replaces that document property (at most 255 characters), and NUMWORDS, NUMCHARS and REVNUM take a `\*` number format |
|
||||
| 2026-10-24 | Field capitalization switches: `\* Upper`, `\* Lower`, `\* Caps` and `\* FirstCap` apply to the document information, DOCPROPERTY, FILENAME, date, REF, PAGEREF, SEQ, STYLEREF and table formula results that Update Field computes and to the page-number fields, and `\* CardText` and `\* OrdText` results are lowercase unless one of them follows (`\* CardText \* Caps` shows "Seven Hundred Ninety"); a table formula takes a `\*` number format (`=SUM(A1:B2) \* CardText`), and other `\*` switches such as `\* MERGEFORMAT` no longer make it a syntax error |
|
||||
| 2026-10-24 | Field `\* DollarText` and `\* Hex` switches: `{ = 9.20 + 5.35 \* DollarText \* Upper }` shows "FOURTEEN AND 55/100" and `\* Hex` writes uppercase hexadecimal digits (458 shows "1CA"), in table formulas and in the page-number, SEQ, PAGEREF and count fields; `{ PAGE \* ArabicDash }` shows "- 31 -"; cardinal, ordinal and dollar text past 999,999 keep their digits |
|
||||
| 2026-10-12 | Undo history list (#701): a ▾ beside the Quick Access Toolbar's Undo lists recent steps newest first; hovering highlights what a click undoes ("Undo 3 Actions"), and a click undoes them in one go. `edit.undoTo {count}` and `edit.undoHistory` (newest first, up to 100) for CLI and MCP |
|
||||
| 2026-10-12 | Start screen (#766): launched with nothing to open, WordCraft starts on File › Home (Blank document, templates, Recent) like Word, desktop and web; Esc leaves a blank document; File › Options › General › Start-up turns it off |
|
||||
| 2026-10-13 | No document behind the Start screen (#1129): Esc or Back from it shows an empty window (no page, ribbon greyed out but for Help) until Blank document, a template or a file is opened, as in Word; closing it asks nothing |
|
||||
|
||||
@@ -109,9 +109,12 @@ pub fn evaluate_field_with(
|
||||
/// with the `\#` picture, or in the cell's language when there is no picture. A number format such
|
||||
/// as `\* CardText`, `\* OrdText` or `\* roman` writes a whole number the format covers (up to
|
||||
/// 999,999 for CardText and OrdText, 1 to 50,000 for roman, 1 to 780 for alphabetic), in lowercase
|
||||
/// for CardText and OrdText. Any other result keeps its digits. A capitalization switch
|
||||
/// (`\* Upper`, `\* Lower`, `\* Caps`, `\* FirstCap`) then applies to the text. Other `\*` switches
|
||||
/// leave the text as it is. Quoted text, such as a `\#` picture, is not a switch.
|
||||
/// for CardText and OrdText. `\* Hex` writes a whole number from 0 to 4,294,967,295 in uppercase
|
||||
/// hexadecimal digits. `\* DollarText` writes a result from 0 to 999,999.99 in lowercase cardinal
|
||||
/// text with its first two decimals (rounded) over 100 (see
|
||||
/// [`wordcraft_layout::fields::dollar_text`]). Any other result keeps its digits. A capitalization
|
||||
/// switch (`\* Upper`, `\* Lower`, `\* Caps`, `\* FirstCap`) then applies to the text. Other `\*`
|
||||
/// switches leave the text as it is. Quoted text, such as a `\#` picture, is not a switch.
|
||||
fn format_switches(switches: &str, value: f64, text: String) -> String {
|
||||
use wordcraft_doc::section::NumFormat;
|
||||
let unquoted = switches.split('"').step_by(2).collect::<Vec<_>>().join(" ");
|
||||
@@ -123,6 +126,10 @@ fn format_switches(switches: &str, value: f64, text: String) -> String {
|
||||
_ => 0.0..=f64::from(u32::MAX),
|
||||
};
|
||||
match fmt {
|
||||
Some(NumFormat::DollarText) => {
|
||||
let text = wordcraft_layout::fields::dollar_text(value).unwrap_or(text);
|
||||
wordcraft_layout::fields::apply_case_switch(switches, text)
|
||||
}
|
||||
Some(fmt) if value.fract() == 0.0 && covers(fmt).contains(&value) => wordcraft_layout::fields::field_number(switches, fmt, value as u32),
|
||||
_ => wordcraft_layout::fields::apply_case_switch(switches, text),
|
||||
}
|
||||
@@ -1005,6 +1012,24 @@ mod tests {
|
||||
assert_eq!(shown("=A1 \\* alphabetic"), "790", "a number past alphabetic keeps its digits");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dollar_text_and_hex_switches_write_the_result() {
|
||||
let t = table(&[&["9.20", "5.35"], &["458", "-1"], &["", ""]]);
|
||||
let shown = |f: &str| evaluate_field(&t, 2, 0, f).unwrap_or_else(|e| panic!("{f}: {e}"));
|
||||
assert_eq!(shown("= 9.20 + 5.35 \\* DollarText \\* Upper"), "FOURTEEN AND 55/100");
|
||||
assert_eq!(shown("=A1+B1 \\* DollarText"), "fourteen and 55/100");
|
||||
assert_eq!(shown("=A2 \\* DollarText \\* FirstCap"), "Four hundred fifty-eight and 00/100");
|
||||
assert_eq!(shown("=0.999 \\* DollarText"), "one and 00/100");
|
||||
assert_eq!(shown("=A2 \\* Hex"), "1CA");
|
||||
assert_eq!(shown("=A2 \\* Hex \\* Lower"), "1ca");
|
||||
assert_eq!(shown("=A2*0 \\* Hex"), "0");
|
||||
assert_eq!(shown("=B2 \\* DollarText"), shown("=B2"), "a negative number keeps its digits");
|
||||
assert_eq!(shown("=B2 \\* Hex"), shown("=B2"));
|
||||
assert_eq!(shown("=A2/4 \\* Hex"), shown("=A2/4"), "a fraction keeps its digits");
|
||||
assert_eq!(shown("=A2*2200 \\* DollarText"), shown("=A2*2200"), "a number past DollarText keeps its digits");
|
||||
assert_eq!(shown("=A2 \\* ArabicDash"), "458", "ArabicDash is for page numbers only");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_huge_range_over_a_small_table_reads_only_the_table() {
|
||||
let t = table(&[&["1", "2", "3"], &["4", "5", "6"], &["", "", ""]]);
|
||||
|
||||
@@ -8798,6 +8798,27 @@ fn table_formula_results_take_the_format_switches() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn table_formula_results_take_dollar_text_and_hex() {
|
||||
for (formula, inserted, updated) in [
|
||||
(r"=SUM(ABOVE) \* DollarText", "twelve and 00/100", "twenty-one and 00/100"),
|
||||
(r"=SUM(ABOVE) \* DollarText \* Upper", "TWELVE AND 00/100", "TWENTY-ONE AND 00/100"),
|
||||
(r"=SUM(ABOVE) \* Hex", "C", "15"),
|
||||
] {
|
||||
let mut s = formula_table();
|
||||
let r = run(&mut s, "table.formula", json!({"formula": formula}));
|
||||
assert_eq!((r["formula"].as_str(), r["result"].as_str()), (Some(formula), Some(inserted)));
|
||||
run(
|
||||
&mut s,
|
||||
"select.range",
|
||||
json!({"anchor": {"story": "body", "path": [1, 1, 1, 0], "off": 0}, "focus": {"story": "body", "path": [1, 1, 1, 0], "off": 1}}),
|
||||
);
|
||||
run(&mut s, "text.insert", json!({"text": "10"}));
|
||||
run(&mut s, "references.updateFields", json!({}));
|
||||
assert_eq!(formula_result(&s, 4, 1).as_deref(), Some(updated), "{formula}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A one-column table (no header row) holding `cells`, in a document whose default language
|
||||
/// is `lang`; the caret ends in the last cell.
|
||||
fn column_table(lang: &str, cells: &[&str]) -> Session {
|
||||
|
||||
@@ -125,8 +125,20 @@ pub(crate) fn reference_link(instr: &str) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
/// `\* FORMAT` switch value (Arabic, roman, ROMAN, alphabetic, ALPHABETIC, CardText, Ordinal…).
|
||||
/// `\* FORMAT` switch value (Arabic, roman, ROMAN, alphabetic, ALPHABETIC, CardText, Ordinal,
|
||||
/// DollarText, Hex…).
|
||||
pub fn format_switch(instr: &str) -> Option<NumFormat> {
|
||||
format_switch_of(instr, false)
|
||||
}
|
||||
|
||||
/// A PAGE field's `\* FORMAT` switch value: the values of [`format_switch`] and `\* ArabicDash`
|
||||
/// (`{ PAGE \* ArabicDash }` shows "- 31 -"). Only PAGE reads `\* ArabicDash`; other fields,
|
||||
/// PAGEREF included, skip it and read the next format switch.
|
||||
pub fn page_format_switch(instr: &str) -> Option<NumFormat> {
|
||||
format_switch_of(instr, true)
|
||||
}
|
||||
|
||||
fn format_switch_of(instr: &str, page: bool) -> Option<NumFormat> {
|
||||
let mut it = instr.split_whitespace();
|
||||
while let Some(w) = it.next() {
|
||||
if let Some(rest) = w.strip_prefix("\\*") {
|
||||
@@ -141,6 +153,9 @@ pub fn format_switch(instr: &str) -> Option<NumFormat> {
|
||||
"OrdText" | "ordtext" => NumFormat::OrdinalText,
|
||||
"Ordinal" | "ordinal" => NumFormat::Ordinal,
|
||||
"Arabic" | "arabic" => NumFormat::Decimal,
|
||||
"ArabicDash" | "arabicdash" | "ARABICDASH" if page => NumFormat::NumberInDash,
|
||||
"DollarText" | "dollartext" | "DOLLARTEXT" => NumFormat::DollarText,
|
||||
"Hex" | "hex" | "HEX" => NumFormat::Hex,
|
||||
// Chinese numerals: counting (一百零一), legal (壹佰零壹), digit by digit (一〇一).
|
||||
v if v.eq_ignore_ascii_case("ChineseNum1") => NumFormat::ChineseCountingThousand,
|
||||
v if v.eq_ignore_ascii_case("ChineseNum2") => NumFormat::ChineseLegalSimplified,
|
||||
@@ -228,17 +243,38 @@ pub fn apply_case_switch(instr: &str, text: String) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
/// Formats `n` in `fmt` as a field shows it. Cardinal and ordinal text is lowercase
|
||||
/// (`\* CardText` shows "twenty-one"), and the field's capitalization switch applies after that
|
||||
/// (`\* CardText \* Caps` shows "Twenty-one").
|
||||
/// Formats `n` in `fmt` as a field shows it. Cardinal, ordinal and dollar text is lowercase
|
||||
/// (`\* CardText` shows "twenty-one", `\* DollarText` shows "twenty-one and 00/100") and writes
|
||||
/// numbers up to 999,999; a larger number keeps its digits. `\* Hex` writes uppercase hexadecimal
|
||||
/// digits (`1CA`). The field's capitalization switch applies after that (`\* CardText \* Caps`
|
||||
/// shows "Twenty-one").
|
||||
pub fn field_number(instr: &str, fmt: NumFormat, n: u32) -> String {
|
||||
let text = match fmt {
|
||||
NumFormat::CardinalText | NumFormat::OrdinalText | NumFormat::DollarText if n > MAX_TEXT_NUMBER => n.to_string(),
|
||||
NumFormat::CardinalText | NumFormat::OrdinalText => fmt.format(n).to_lowercase(),
|
||||
NumFormat::DollarText => dollar_text(f64::from(n)).unwrap_or_else(|| n.to_string()),
|
||||
NumFormat::Hex => format!("{n:X}"),
|
||||
_ => fmt.format(n),
|
||||
};
|
||||
apply_case_switch(instr, text)
|
||||
}
|
||||
|
||||
/// The largest number cardinal, ordinal and dollar text write in words.
|
||||
const MAX_TEXT_NUMBER: u32 = 999_999;
|
||||
|
||||
/// `value` as `\* DollarText` writes it, before any capitalization switch: the whole part in
|
||||
/// lowercase cardinal text, "and", and the first two decimals (rounded) over 100
|
||||
/// (14.55 gives "fourteen and 55/100"). `None` for NaN, or for a value that rounds below 0 or past
|
||||
/// 999,999.99.
|
||||
pub fn dollar_text(value: f64) -> Option<String> {
|
||||
let cents = (value * 100.0).round();
|
||||
if !(0.0..=f64::from(MAX_TEXT_NUMBER) * 100.0 + 99.0).contains(¢s) {
|
||||
return None;
|
||||
}
|
||||
let cents = cents as u32;
|
||||
Some(format!("{} and {:02}/100", NumFormat::CardinalText.format(cents / 100).to_lowercase(), cents % 100))
|
||||
}
|
||||
|
||||
/// The light grey behind a field shown as its code.
|
||||
pub const FIELD_SHADING: wordcraft_doc::props::Rgb = wordcraft_doc::props::Rgb(0xD9, 0xD9, 0xD9);
|
||||
|
||||
@@ -257,7 +293,7 @@ pub fn field_text_on(instr: &str, result: &str, ctx: &FieldCtx, page: u32) -> (S
|
||||
let name = field_name(instr);
|
||||
let fmt = format_switch(instr);
|
||||
match name.as_str() {
|
||||
"PAGE" => (field_number(instr, fmt.unwrap_or(ctx.page_format), page), true),
|
||||
"PAGE" => (field_number(instr, page_format_switch(instr).unwrap_or(ctx.page_format), page), true),
|
||||
"NUMPAGES" => (field_number(instr, fmt.unwrap_or(NumFormat::Decimal), ctx.pages), true),
|
||||
"SECTIONPAGES" => (field_number(instr, fmt.unwrap_or(NumFormat::Decimal), ctx.section_pages), true),
|
||||
"SECTION" => (field_number(instr, fmt.unwrap_or(NumFormat::Decimal), ctx.section), true),
|
||||
@@ -404,6 +440,36 @@ mod tests {
|
||||
assert_eq!(field_text(r"REF mark \* Upper", "stored", &ctx).0, "stored", "a stored result shows as it is");
|
||||
}
|
||||
|
||||
/// `\* DollarText` writes lowercase cardinal text with "and 00/100", `\* Hex` uppercase
|
||||
/// hexadecimal digits, and `\* ArabicDash` a page number between hyphens, on PAGE only. Words
|
||||
/// stop at 999,999; a larger number keeps its digits.
|
||||
#[test]
|
||||
fn dollar_text_hex_and_arabic_dash_switches() {
|
||||
let ctx = FieldCtx { page: 31, pages: 458, section: 790, ..Default::default() };
|
||||
let text = |instr: &str| field_text(instr, "", &ctx).0;
|
||||
assert_eq!(text(r"PAGE \* ArabicDash"), "- 31 -");
|
||||
assert_eq!(text(r"PAGE \*arabicdash \* MERGEFORMAT"), "- 31 -");
|
||||
assert_eq!(text(r"NUMPAGES \* ArabicDash"), "458", "ArabicDash is for page numbers only");
|
||||
assert_eq!(text(r"NUMPAGES \* ArabicDash \* roman"), "cdlviii", "the next format switch counts");
|
||||
assert_eq!(text(r"NUMPAGES \* Hex"), "1CA");
|
||||
assert_eq!(text(r"NUMPAGES \* hex \* Lower"), "1ca");
|
||||
assert_eq!(text(r"PAGE \* Hex"), "1F");
|
||||
assert_eq!(text(r"SECTION \* DollarText"), "seven hundred ninety and 00/100");
|
||||
assert_eq!(text(r"SECTION \* DollarText \* Upper"), "SEVEN HUNDRED NINETY AND 00/100");
|
||||
assert_eq!(text(r"PAGE \* DollarText \* FirstCap"), "Thirty-one and 00/100");
|
||||
assert_eq!(dollar_text(9.20 + 5.35).as_deref(), Some("fourteen and 55/100"));
|
||||
assert_eq!(dollar_text(0.999).as_deref(), Some("one and 00/100"), "the decimals round");
|
||||
assert_eq!(dollar_text(999_999.99).as_deref(), Some("nine hundred ninety-nine thousand nine hundred ninety-nine and 99/100"));
|
||||
assert_eq!(dollar_text(999_999.995), None);
|
||||
assert_eq!(dollar_text(-1.0), None);
|
||||
assert_eq!(dollar_text(-0.004).as_deref(), Some("zero and 00/100"), "a value that rounds to 0 is not below 0");
|
||||
assert_eq!(dollar_text(f64::NAN), None);
|
||||
for fmt in [NumFormat::CardinalText, NumFormat::OrdinalText, NumFormat::DollarText] {
|
||||
assert_eq!(field_number(r"SEQ x \* Upper", fmt, 1_000_140), "1000140", "{fmt:?}");
|
||||
}
|
||||
assert_eq!(field_number("SEQ x", NumFormat::CardinalText, 999_999), "nine hundred ninety-nine thousand nine hundred ninety-nine");
|
||||
}
|
||||
|
||||
/// #1765: ASK and SET only assign text to a bookmark; they show nothing, whatever result is stored.
|
||||
#[test]
|
||||
fn ask_and_set_show_nothing() {
|
||||
|
||||
Reference in New Issue
Block a user