use super::plural;
use crate::spec::Locale;
use std::fmt::Write as _;
pub(crate) fn series_at(locale: Locale, name: &str, category: &str) -> String {
match locale {
Locale::En => format!("{name} in {category}"),
Locale::De => format!("{name}, {category}"),
}
}
pub(crate) fn zone_times(locale: Locale, from: Option<&str>, to: Option<&str>) -> Option<String> {
match (locale, from, to) {
(Locale::En, Some(from), Some(to)) => Some(format!("{from} to {to}")),
(Locale::De, Some(from), Some(to)) => Some(format!("{from} bis {to}")),
(Locale::En, Some(from), None) => Some(format!("from {from}")),
(Locale::De, Some(from), None) => Some(format!("ab {from}")),
(Locale::En, None, Some(to)) => Some(format!("until {to}")),
(Locale::De, None, Some(to)) => Some(format!("bis {to}")),
(_, None, None) => None,
}
}
pub(crate) fn zone_values(
locale: Locale,
bottom: Option<&str>,
top: Option<&str>,
) -> Option<String> {
match (locale, bottom, top) {
(Locale::En, Some(bottom), Some(top)) => Some(format!("values {bottom} to {top}")),
(Locale::De, Some(bottom), Some(top)) => Some(format!("Werte {bottom} bis {top}")),
(Locale::En, Some(bottom), None) => Some(format!("values above {bottom}")),
(Locale::De, Some(bottom), None) => Some(format!("Werte über {bottom}")),
(Locale::En, None, Some(top)) => Some(format!("values below {top}")),
(Locale::De, None, Some(top)) => Some(format!("Werte unter {top}")),
(_, None, None) => None,
}
}
pub(crate) fn time_opening(
locale: Locale,
points: usize,
range: &str,
names: &[String],
modeled: bool,
) -> String {
let point = plural(points, "point", "points");
let punkt = plural(points, "Punkt", "Punkten");
match (locale, names.len() > 1) {
(Locale::En, true) => format!(
"Time chart with {points} {point} {range} and {} series ({}).",
names.len(),
names.join(", ")
),
(Locale::De, true) => format!(
"Zeitreihe mit {points} {punkt} {range} und {} Reihen ({}).",
names.len(),
names.join(", ")
),
(Locale::En, false) if modeled => {
format!("Time chart with {points} {point} {range}, modelled.")
}
(Locale::De, false) if modeled => {
format!("Zeitreihe mit {points} {punkt} {range}, modelliert.")
}
(Locale::En, false) => format!("Time chart with {points} {point} {range}."),
(Locale::De, false) => format!("Zeitreihe mit {points} {punkt} {range}."),
}
}
pub(crate) fn axis_noun(locale: Locale, zone: crate::time::TimeZone, opening: String) -> String {
if !zone.is_numeric() {
return opening;
}
let (time, line) = match locale {
Locale::En => ("Time chart", "Line chart"),
Locale::De => ("Zeitreihe", "Liniendiagramm"),
};
match opening.strip_prefix(time) {
Some(rest) => format!("{line}{rest}"),
None => opening,
}
}
pub(crate) fn multiples_opening(
locale: Locale,
panels: &[String],
range: &str,
names: &[String],
shared: bool,
) -> String {
let axis = match (locale, shared) {
(Locale::En, true) => "a shared value axis",
(Locale::En, false) => "a value axis of their own",
(Locale::De, true) => "gemeinsamer Werteachse",
(Locale::De, false) => "je eigener Werteachse",
};
let mut text = match locale {
Locale::En => format!(
"Time chart in {} {} ({}) with {axis}, each {range}",
panels.len(),
plural(panels.len(), "panel", "panels"),
panels.join(", ")
),
Locale::De => format!(
"Zeitreihe in {} {} ({}) mit {axis}, jeweils {range}",
panels.len(),
plural(panels.len(), "Feld", "Feldern"),
panels.join(", ")
),
};
if names.len() > 1 {
let showing = match locale {
Locale::En => "showing",
Locale::De => "mit",
};
text.push_str(", ");
text.push_str(showing);
text.push(' ');
text.push_str(&names.join(", "));
}
text.push('.');
text
}
pub(crate) fn panes(locale: Locale, labels: &[String]) -> String {
match locale {
Locale::En => format!(
" {} stacked panes on one time axis: {}.",
labels.len(),
labels.join(", ")
),
Locale::De => format!(
" {} übereinanderliegende Teildiagramme auf einer Zeitachse: {}.",
labels.len(),
labels.join(", ")
),
}
}
pub(crate) fn candle_values(locale: Locale, values: &[String; 4]) -> String {
let words = locale.words();
let separator = match locale {
Locale::En => ", ",
Locale::De => "; ",
};
[words.open, words.high, words.low, words.close]
.iter()
.zip(values)
.map(|(word, value)| format!("{word} {value}"))
.collect::<Vec<_>>()
.join(separator)
}
pub(crate) struct CandleSummary<'a> {
pub name: Option<&'a str>,
pub open: (&'a str, &'a str),
pub close: (&'a str, &'a str),
pub change: &'a str,
pub percent: Option<&'a str>,
pub high: (&'a str, &'a str),
pub low: (&'a str, &'a str),
}
pub(crate) fn candles(locale: Locale, summary: &CandleSummary) -> String {
let percent = summary
.percent
.map_or_else(String::new, |percent| format!(" ({percent})"));
let name = summary
.name
.map_or_else(String::new, |name| format!("{name}: "));
let CandleSummary {
open,
close,
high,
low,
change,
..
} = summary;
match (locale, summary.name.is_some()) {
(Locale::En, named) => format!(
" {name}{} at {} on {} and closed at {} on {}, a change of {change}{percent}; high {} on {}, low {} on {}.",
if named { "opened" } else { "Opened" },
open.0,
open.1,
close.0,
close.1,
high.0,
high.1,
low.0,
low.1,
),
(Locale::De, _) => format!(
" {name}Eröffnung {} am {}, Schluss {} am {}, Veränderung {change}{percent}; Hoch {} am {}, Tief {} am {}.",
open.0, open.1, close.0, close.1, high.0, high.1, low.0, low.1,
),
}
}
pub(crate) fn bands(locale: Locale, banded: usize, hatched: bool) -> String {
match locale {
Locale::En => format!(
" {} a shaded band from {} lower to {} upper bound{}; the data table lists both.",
plural(banded, "One line has", "Lines have"),
plural(banded, "its", "their"),
plural(banded, "its", "their"),
if hatched {
", hatched where modelled"
} else {
""
}
),
Locale::De => format!(
" {} ein schattiertes Band von der unteren bis zur oberen Grenze{}; die Datentabelle nennt beide.",
plural(banded, "Eine Linie hat", "Linien haben je"),
if hatched {
", schraffiert, wo modelliert"
} else {
""
}
),
}
}
pub(crate) fn panel_bars_opening(locale: Locale, panels: usize, categories: usize) -> String {
match locale {
Locale::En => format!(
"Bar chart in {panels} {} over {categories} {}, each with its own value axis.",
plural(panels, "panel", "panels"),
plural(categories, "category", "categories")
),
Locale::De => format!(
"Balkendiagramm in {panels} {} über {categories} {}, jedes mit eigener Wertachse.",
plural(panels, "Feld", "Feldern"),
plural(categories, "Kategorie", "Kategorien")
),
}
}
pub(crate) fn panel_notes(locale: Locale, notes: &str) -> String {
match locale {
Locale::En => format!(" Notes: {notes}."),
Locale::De => format!(" Hinweise: {notes}."),
}
}
pub(crate) fn stacked_areas(locale: Locale, names: &str) -> String {
match locale {
Locale::En => {
format!(" Stacked areas, from the bottom up: {names}; the top edge shows their total.")
}
Locale::De => format!(
" Gestapelte Flächen, von unten nach oben: {names}; die Oberkante zeigt ihre Summe."
),
}
}
pub(crate) fn areas(locale: Locale, names: &[String]) -> String {
match (locale, names.len()) {
(Locale::En, 0) => " The area below the line is filled down to zero.".to_owned(),
(Locale::En, 1) => format!(" The area below {} is filled down to zero.", names[0]),
(Locale::En, _) => format!(
" The areas below {} are filled down to zero.",
names.join(", ")
),
(Locale::De, 0) => " Die Fläche unter der Linie ist bis null gefüllt.".to_owned(),
(Locale::De, 1) => format!(" Die Fläche unter {} ist bis null gefüllt.", names[0]),
(Locale::De, _) => format!(
" Die Flächen unter {} sind bis null gefüllt.",
names.join(", ")
),
}
}
const fn chart_name(locale: Locale, line: bool) -> &'static str {
match (locale, line) {
(Locale::En, false) => "Bar chart",
(Locale::En, true) => "Line chart",
(Locale::De, false) => "Balkendiagramm",
(Locale::De, true) => "Liniendiagramm",
}
}
pub(crate) fn equal_values(locale: Locale, line: bool, categories: usize, value: &str) -> String {
let chart = chart_name(locale, line);
match locale {
Locale::En if categories == 1 => format!("{chart} with 1 value: {value}."),
Locale::En => format!("{chart} with {categories} equal values: {value} each."),
Locale::De if categories == 1 => format!("{chart} mit 1 Wert: {value}."),
Locale::De => format!("{chart} mit {categories} gleichen Werten: jeweils {value}."),
}
}
pub(crate) fn categories_opening(
locale: Locale,
line: bool,
categories: usize,
series: usize,
names: &str,
) -> String {
let chart = chart_name(locale, line);
match (locale, series > 1) {
(Locale::En, true) => format!(
"{chart} with {categories} {} and {series} series ({names}).",
plural(categories, "category", "categories")
),
(Locale::De, true) => format!(
"{chart} mit {categories} {} und {series} Reihen ({names}).",
plural(categories, "Kategorie", "Kategorien")
),
(Locale::En, false) => format!(
"{chart} with {categories} {}.",
plural(categories, "category", "categories")
),
(Locale::De, false) => format!(
"{chart} mit {categories} {}.",
plural(categories, "Kategorie", "Kategorien")
),
}
}
pub(crate) fn missing_values(locale: Locale, missing: usize) -> String {
match (locale, missing) {
(_, 0) => String::new(),
(Locale::En, 1) => " 1 value is missing.".to_owned(),
(Locale::En, missing) => format!(" {missing} values are missing."),
(Locale::De, 1) => " 1 Wert fehlt.".to_owned(),
(Locale::De, missing) => format!(" {missing} Werte fehlen."),
}
}
pub(crate) fn stacked_totals(
locale: Locale,
(highest, highest_at): (&str, &str),
(lowest, lowest_at): (&str, &str),
) -> String {
match locale {
Locale::En => format!(
" The series are stacked. Highest total: {highest} ({highest_at}). Lowest total: {lowest} ({lowest_at})."
),
Locale::De => format!(
" Die Reihen sind gestapelt. Höchste Summe: {highest} ({highest_at}). Niedrigste Summe: {lowest} ({lowest_at})."
),
}
}
pub(crate) const fn stacked_shares(locale: Locale) -> &'static str {
match locale {
Locale::En => " Each bar shows the shares of the series in its category's total.",
Locale::De => " Jeder Balken zeigt die Anteile der Reihen an der Summe seiner Kategorie.",
}
}
pub(crate) const fn gaps_collapsed(locale: Locale) -> &'static str {
match locale {
Locale::En => " Gaps in time are closed up.",
Locale::De => " Zeiträume ohne Beobachtung sind ausgelassen.",
}
}
pub(crate) fn diverging_scale(locale: Locale, reference: &str, min: &str, max: &str) -> String {
match locale {
Locale::En => format!(
"on a diverging color scale around {reference} with its outermost steps at {min} and {max}"
),
Locale::De => format!(
"auf einer divergierenden Farbskala um {reference} mit den äußersten Stufen bei {min} und {max}"
),
}
}
pub(crate) fn stripes_opening(locale: Locale, first: i32, last: i32, scale: &str) -> String {
match locale {
Locale::En if first == last => format!("Warming stripes for {first}, one stripe, {scale}."),
Locale::De if first == last => format!("Wärmestreifen für {first}, ein Streifen, {scale}."),
Locale::En => {
format!("Warming stripes from {first} to {last}, one stripe per year, {scale}.")
}
Locale::De => {
format!("Wärmestreifen von {first} bis {last}, ein Streifen pro Jahr, {scale}.")
}
}
}
pub(crate) fn years_without_value(locale: Locale, missing: usize) -> String {
match (locale, missing) {
(_, 0) => String::new(),
(Locale::En, 1) => " 1 year has no value.".to_owned(),
(Locale::En, missing) => format!(" {missing} years have no value."),
(Locale::De, 1) => " 1 Jahr hat keinen Wert.".to_owned(),
(Locale::De, missing) => format!(" {missing} Jahre haben keinen Wert."),
}
}
pub(crate) fn calendar_opening(
locale: Locale,
year: i32,
weeks: bool,
scale: &str,
days: usize,
length: i64,
) -> String {
match locale {
Locale::En => format!(
"Calendar of {year} with {}, {scale}. {days} of {length} days {} a value.",
if weeks {
"one row per weekday and one column per week"
} else {
"one row per month"
},
plural(days, "has", "have")
),
Locale::De => format!(
"Kalender {year} mit {}, {scale}. {days} von {length} Tagen {} einen Wert.",
if weeks {
"einer Zeile pro Wochentag und einer Spalte pro Woche"
} else {
"einer Zeile pro Monat"
},
plural(days, "hat", "haben")
),
}
}
pub(crate) fn rangebar_opening(
locale: Locale,
categories: usize,
mid: bool,
lowest: (&str, &str),
highest: (&str, &str),
) -> String {
match locale {
Locale::En => format!(
"Range chart with {categories} {}, each a span from low to high{}. Lowest low: {} ({}). Highest high: {} ({}).",
plural(categories, "category", "categories"),
if mid { " and a central value" } else { "" },
lowest.0,
lowest.1,
highest.0,
highest.1,
),
Locale::De => format!(
"Spannweitendiagramm mit {categories} {}, jeweils eine Spanne vom unteren zum oberen Wert{}. Niedrigster unterer Wert: {} ({}). Höchster oberer Wert: {} ({}).",
plural(categories, "Kategorie", "Kategorien"),
if mid { " mit einem mittleren Wert" } else { "" },
lowest.0,
lowest.1,
highest.0,
highest.1,
),
}
}
pub(crate) fn range_groups(locale: Locale, groups: &str) -> String {
match locale {
Locale::En => format!(" Colors show groups: {groups}."),
Locale::De => format!(" Farben zeigen Gruppen: {groups}."),
}
}
pub(crate) fn modeled_ranges(locale: Locale, labels: &str) -> String {
match locale {
Locale::En => format!(" Modelled, drawn hatched: {labels}."),
Locale::De => format!(" Modelliert, schraffiert gezeichnet: {labels}."),
}
}
pub(crate) fn error_bars(locale: Locale, line: bool) -> String {
match (locale, line) {
(Locale::En, false) => " Error bars show the interval from lower to upper; the table gives it for every bar.".to_owned(),
(Locale::En, true) => " Error bars show the interval from lower to upper; the table gives it for every point.".to_owned(),
(Locale::De, false) => " Fehlerbalken zeigen das Intervall von unten bis oben; die Tabelle nennt es für jeden Balken.".to_owned(),
(Locale::De, true) => " Fehlerbalken zeigen das Intervall von unten bis oben; die Tabelle nennt es für jeden Punkt.".to_owned(),
}
}
pub(crate) fn summary_ranges(locale: Locale, summaries: &str) -> String {
match locale {
Locale::En => format!(" Overall result, drawn as a diamond: {summaries}."),
Locale::De => format!(" Gesamtergebnis, als Raute gezeichnet: {summaries}."),
}
}
pub(crate) fn weighted_ranges(locale: Locale) -> String {
match locale {
Locale::En => " The area of each square follows the weight of its span.".to_owned(),
Locale::De => " Die Fläche jedes Quadrats folgt dem Gewicht seiner Spanne.".to_owned(),
}
}
pub(crate) fn timeline_opening(
locale: Locale,
(phases, milestones): (usize, usize),
(first, last): (&str, &str),
) -> String {
let days = match locale {
Locale::En if first == last => format!("on {first}"),
Locale::De if first == last => format!("am {first}"),
Locale::En => format!("from {first} to {last}"),
Locale::De => format!("vom {first} bis zum {last}"),
};
match locale {
Locale::En => format!(
"Timeline with {phases} phase{} and {milestones} milestone{} {days}.",
plural(phases, "", "s"),
plural(milestones, "", "s"),
),
Locale::De => format!(
"Zeitstrahl mit {phases} {} und {milestones} {} {days}.",
plural(phases, "Phase", "Phasen"),
plural(milestones, "Meilenstein", "Meilensteinen"),
),
}
}
pub(crate) fn timeline_item(locale: Locale, label: &str, days: &str, follows: &[&str]) -> String {
let after = match (locale, follows.is_empty()) {
(_, true) => String::new(),
(Locale::En, false) => format!(", after {}", follows.join(" and ")),
(Locale::De, false) => format!(", nach {}", follows.join(" und ")),
};
format!("{label}: {days}{after}.")
}
pub(crate) fn timeline_markers(locale: Locale, markers: &str) -> String {
match locale {
Locale::En => format!(" Marked days: {markers}."),
Locale::De => format!(" Markierte Tage: {markers}."),
}
}
pub(crate) fn waterfall_summary(
locale: Locale,
(steps, end): (usize, &str),
rise: Option<(&str, String)>,
fall: Option<(&str, String)>,
) -> String {
let (rise_word, fall_word) = match locale {
Locale::En => ("Biggest rise", "Biggest fall"),
Locale::De => ("Größter Anstieg", "Größter Rückgang"),
};
let mut text = match locale {
Locale::En => format!("Waterfall with {steps} steps, ending at {end}."),
Locale::De => format!("Wasserfall mit {steps} Schritten, endend bei {end}."),
};
for (word, entry) in [(rise_word, rise), (fall_word, fall)] {
if let Some((label, value)) = entry {
write!(text, " {word}: {label} ({value}).").expect("writing to String cannot fail");
}
}
text
}
pub(crate) fn waffle_summary(locale: Locale, (squares, each): (u32, &str), parts: &str) -> String {
match locale {
Locale::En => format!(
"Waffle chart of {squares} squares, each standing for {each}. Parts, as value (share, squares): {parts}."
),
Locale::De => format!(
"Waffeldiagramm aus {squares} Feldern, jedes für {each}. Teile als Wert (Anteil, Felder): {parts}."
),
}
}
pub(crate) fn parliament_summary(
locale: Locale,
(seats, parties): (u32, usize),
list: &str,
majority: Option<u32>,
) -> String {
let majority = match (locale, majority) {
(_, None) => String::new(),
(Locale::En, Some(seats)) => format!(" A majority takes {seats} seats."),
(Locale::De, Some(seats)) => format!(" Eine Mehrheit braucht {seats} Sitze."),
};
match locale {
Locale::En => format!(
"Parliament with {seats} seats and {parties} part{}: {list}.{majority}",
plural(parties, "y", "ies")
),
Locale::De => format!(
"Parlament mit {seats} Sitzen und {parties} {}: {list}.{majority}",
plural(parties, "Partei", "Parteien")
),
}
}
pub(crate) fn parliament_coalition(
locale: Locale,
parties: &str,
seats: u32,
majority: bool,
) -> String {
match (locale, majority) {
(Locale::En, true) => format!("Coalition {parties}: {seats} seats, a majority."),
(Locale::En, false) => format!("Coalition {parties}: {seats} seats, no majority."),
(Locale::De, true) => format!("Koalition {parties}: {seats} Sitze, eine Mehrheit."),
(Locale::De, false) => format!("Koalition {parties}: {seats} Sitze, keine Mehrheit."),
}
}
pub(crate) fn percent_done(locale: Locale, progress: f64) -> String {
let percent = (progress * 100.0).round();
match locale {
Locale::En => format!("{percent}% done"),
Locale::De => format!("{percent} % erledigt"),
}
}
pub(crate) fn sankey_summary(
locale: Locale,
(nodes, links): (usize, usize),
total: &str,
largest: &str,
) -> String {
match locale {
Locale::En => format!(
"Sankey diagram of {nodes} nodes and {links} link{}; {total} enter it. Biggest links: {largest}.",
plural(links, "", "s")
),
Locale::De => format!(
"Sankey-Diagramm aus {nodes} Knoten und {links} {}; {total} fließen hinein. Größte Verbindungen: {largest}.",
plural(links, "Verbindung", "Verbindungen")
),
}
}
pub(crate) fn treemap_summary(
locale: Locale,
(items, total): (usize, &str),
largest: &str,
groups: &str,
) -> String {
let groups = match (locale, groups.is_empty()) {
(_, true) => String::new(),
(Locale::En, false) => format!(" Groups: {groups}."),
(Locale::De, false) => format!(" Gruppen: {groups}."),
};
match locale {
Locale::En => format!(
"Treemap of {items} item{}, together {total}; the rectangle of each has the area of its value. Largest: {largest}.{groups}",
plural(items, "", "s")
),
Locale::De => format!(
"Treemap aus {items} {}, zusammen {total}; die Fläche jedes Rechtecks folgt seinem Wert. Am größten: {largest}.{groups}",
plural(items, "Eintrag", "Einträgen")
),
}
}
#[cfg(test)]
mod tests {
use super::{bands, multiples_opening, panel_bars_opening};
use crate::spec::Locale;
#[test]
fn panels_are_named_in_plain_words() {
let two = ["A".to_owned(), "B".to_owned()];
assert_eq!(
multiples_opening(Locale::En, &two, "from 2000 to 2050", &[], true),
"Time chart in 2 panels (A, B) with a shared value axis, each from 2000 to 2050."
);
assert_eq!(
multiples_opening(Locale::De, &two, "von 2000 bis 2050", &[], false),
"Zeitreihe in 2 Feldern (A, B) mit je eigener Werteachse, jeweils von 2000 bis 2050."
);
let one = ["A".to_owned()];
assert!(
multiples_opening(Locale::En, &one, "from 2000 to 2050", &[], true)
.starts_with("Time chart in 1 panel (A)")
);
assert!(
multiples_opening(Locale::De, &one, "von 2000 bis 2050", &[], true)
.starts_with("Zeitreihe in 1 Feld (A)")
);
assert_eq!(
panel_bars_opening(Locale::En, 2, 3),
"Bar chart in 2 panels over 3 categories, each with its own value axis."
);
assert_eq!(
panel_bars_opening(Locale::De, 2, 1),
"Balkendiagramm in 2 Feldern über 1 Kategorie, jedes mit eigener Wertachse."
);
}
#[test]
fn bands_agree_with_the_number_of_lines() {
assert_eq!(
bands(Locale::En, 1, true),
" One line has a shaded band from its lower to its upper bound, hatched where modelled; the data table lists both."
);
assert_eq!(
bands(Locale::En, 2, false),
" Lines have a shaded band from their lower to their upper bound; the data table lists both."
);
assert!(bands(Locale::De, 2, false).starts_with(" Linien haben je ein schattiertes Band"));
}
}