use handlebars::{self, handlebars_helper};
use serde_json::Value as Json;
use std::io::Write;
handlebars_helper!(upper: | s: str | s.to_uppercase());
handlebars_helper!(lower: |s:str| s.to_lowercase());
fn sep_helper(
h: &handlebars::Helper,
_: &handlebars::Handlebars,
context: &handlebars::Context,
_: &mut handlebars::RenderContext,
out: &mut dyn handlebars::Output,
) -> handlebars::HelperResult {
if h.param(1).is_some() {
return Err(handlebars::RenderError::new::<String>(
"pm only takes two arguments. More were given.".into(),
));
};
let data = context.data();
let separator = match data.get("separator") {
Some(v) => v.as_str().unwrap(),
None => {
return Err(handlebars::RenderError::new::<String>(
"Could not find the \"separator\" key in the data file. Please add it.".into(),
))
}
};
let value = match h.param(0) {
Some(p) => match p.value().as_str() {
Some(s) => s.to_owned(),
None => p.value().to_string(),
},
None => {
return Err(handlebars::RenderError::new::<String>(
"Could not read the second argument..".into(),
))
}
};
let mut new_value = String::new();
let mut number_buffer = String::new();
let mut in_digit = false;
let mut n_periods = 0;
for c in value.chars() {
if c == '.' {
n_periods += 1;
} else {
n_periods = 0;
};
in_digit = c.is_ascii_digit() | (in_digit & (n_periods == 1));
if in_digit {
number_buffer.push(c);
} else {
if !number_buffer.is_empty() {
let number = number_buffer.parse::<f64>().unwrap();
new_value += &add_separators(number, separator);
number_buffer.clear();
}
new_value.push(c);
};
}
if !number_buffer.is_empty() {
let number = number_buffer.parse::<f64>().unwrap();
new_value += &add_separators(number, separator);
}
out.write(&new_value)?;
Ok(())
}
fn add_separators(number: f64, separator: &str) -> String {
let number_str = format!("{}", number);
let separator_backwards = separator.chars().rev().collect::<String>();
let real_part_str = format!("{}", number.trunc() as i64);
let mut new_real_str_rev = String::new();
for (i, c) in real_part_str
.chars()
.collect::<Vec<char>>()
.iter()
.rev()
.enumerate()
{
if (i % 3 == 0) & (i > 0) {
new_real_str_rev.push_str(&separator_backwards);
};
new_real_str_rev.push(*c);
}
let new_real_str: String = new_real_str_rev.chars().rev().collect::<String>();
number_str.replace(&real_part_str, &new_real_str)
}
fn pm_helper(
h: &handlebars::Helper,
_: &handlebars::Handlebars,
context: &handlebars::Context,
_: &mut handlebars::RenderContext,
out: &mut dyn handlebars::Output,
) -> handlebars::HelperResult {
if h.param(2).is_some() {
return Err(handlebars::RenderError::new::<String>(
"pm only takes two arguments. More were given.".into(),
));
};
let two_arguments = h.param(1).is_some();
let key_index: usize = match two_arguments {
true => 1,
false => 0,
};
let keys = match h.param(key_index) {
Some(attr) => match attr.context_path() {
Some(v) => v,
None => {
let e = match attr.relative_path() {
Some(rp) => format!("pm got invalid data path: {:?}", rp),
None => match attr.value() {
Json::Null => "No argument was found.".to_string(),
v => format!("pm argument: {} is not a valid data path.", v.to_string()),
},
};
return Err(handlebars::RenderError::new::<String>(e));
}
},
None => {
return Err(handlebars::RenderError::new::<String>(
"No argument was given for pm".into(),
))
}
};
let value_key = keys[(keys.len() - 1)].to_owned();
let parent_keys = &keys[..(keys.len() - 1)];
let mut parent: &Json = context.data();
for key in parent_keys {
parent = parent
.get(key)
.expect("Getter failed on parent json. Shouldn't happen.");
}
let mut value = match parent
.get(&value_key)
.expect("Value not found in parent. Something is wrong.")
.as_f64()
{
Some(v) => v,
None => {
return Err(handlebars::RenderError::new::<String>(format!(
"Could not parse value {} as float",
parent.get(&value_key).unwrap()
)))
}
};
let mut pm = match parent.get(&(value_key.to_owned() + "_pm")) {
Some(v) => match v.as_f64() {
Some(y) => y,
None => {
return Err(handlebars::RenderError::new::<String>(format!(
"Could not parse pm value {} as float",
v.to_string()
)))
}
},
None => {
return Err(handlebars::RenderError::new::<String>(format!(
"{}_pm key not found",
value_key
)))
}
};
if two_arguments {
let decimals = match h.param(0) {
Some(p) => {
match json_as_integer(p.value()) {
Ok(x) => x,
Err(e) => return Err(handlebars::RenderError::new::<String>(e)),
}
}
None => {
return Err(handlebars::RenderError::new::<String>(
"Could not find the first argument.".into(),
))
}
};
value = round_value(value, decimals);
pm = round_value(pm, decimals);
}
out.write(&format!("{}$\\pm${}", value, pm))?;
Ok(())
}
fn round_helper(
h: &handlebars::Helper,
_: &handlebars::Handlebars,
_: &handlebars::Context,
_: &mut handlebars::RenderContext,
out: &mut dyn handlebars::Output,
) -> handlebars::HelperResult {
let decimals: i64;
let value: f64;
let two_arguments = h.param(1).is_some();
if two_arguments {
decimals = match h.param(0) {
Some(p) => {
match json_as_integer(p.value()) {
Ok(x) => x,
Err(e) => return Err(handlebars::RenderError::new::<String>(e)),
}
}
None => {
return Err(handlebars::RenderError::new::<String>(
"Could not find the first argument.".into(),
))
}
};
value = match json_as_float(h.param(1).unwrap().value()) {
Ok(x) => x,
Err(e) => return Err(handlebars::RenderError::new::<String>(e)),
}
} else {
decimals = 0;
value = match h.param(0) {
Some(p) => {
match json_as_float(p.value()) {
Ok(x) => x,
Err(e) => return Err(handlebars::RenderError::new::<String>(e)),
}
}
None => {
return Err(handlebars::RenderError::new::<String>(
"Could not read the first argument.".into(),
))
}
};
}
out.write(&format!("{}", round_value(value, decimals)))?;
Ok(())
}
fn roundup_helper(
h: &handlebars::Helper,
_: &handlebars::Handlebars,
_: &handlebars::Context,
_: &mut handlebars::RenderContext,
out: &mut dyn handlebars::Output,
) -> handlebars::HelperResult {
let decimals = match h.param(0) {
Some(p) => match json_as_integer(p.value()) {
Ok(x) => x,
Err(e) => return Err(handlebars::RenderError::new::<String>(e)),
},
None => {
return Err(handlebars::RenderError::new::<String>(
"No arguments provided.".into(),
))
}
};
let value = match h.param(1) {
Some(p) => match json_as_float(p.value()) {
Ok(x) => x,
Err(e) => return Err(handlebars::RenderError::new::<String>(e)),
},
None => {
return Err(handlebars::RenderError::new::<String>(
"Only one argument provided. Requires: 'power' 'value'".into(),
))
}
};
out.write(&format!("{}", round_value(value, -decimals)))?;
Ok(())
}
fn exponent_helper(
h: &handlebars::Helper,
_: &handlebars::Handlebars,
_: &handlebars::Context,
_: &mut handlebars::RenderContext,
out: &mut dyn handlebars::Output,
) -> handlebars::HelperResult {
let value = match h.param(0) {
Some(p) => match json_as_float(p.value()) {
Ok(x) => x,
Err(e) => return Err(handlebars::RenderError::new::<String>(e)),
},
None => {
return Err(handlebars::RenderError::new::<String>(
"No arguments provided.".into(),
))
}
};
let exponent = match h.param(1) {
Some(p) => match json_as_float(p.value()) {
Ok(x) => x,
Err(e) => return Err(handlebars::RenderError::new::<String>(e)),
},
None => {
return Err(handlebars::RenderError::new::<String>(
"No arguments provided.".into(),
))
}
};
let product = match exponent >= 0.0 {
true => value.powf(exponent),
false => 1.0 / value.powf(-exponent),
};
match product.fract() == 0.0 {
true => out.write(&format!("{}", product as i64)),
false => out.write(&format!("{}", product)),
}?;
Ok(())
}
fn json_as_integer(value: &Json) -> Result<i64, String> {
let parsed: Option<i64> = match value {
Json::Number(n) => n.as_i64(),
Json::String(s) => match s.to_string().parse::<i64>() {
Ok(x) => Some(x),
Err(_) => None,
},
_ => None,
};
match parsed {
Some(n) => Ok(n),
None => Err(format!("Could not parse {} as an integer.", value)),
}
}
fn json_as_float(value: &Json) -> Result<f64, String> {
let parsed: Option<f64> = match value {
Json::Number(n) => n.as_f64(),
Json::String(s) => match s.to_string().parse::<f64>() {
Ok(x) => Some(x),
Err(_) => None,
},
_ => None,
};
match parsed {
Some(n) => Ok(n),
None => Err(format!(
"Could not parse {} as a floating point value.",
value
)),
}
}
fn round_value(value: f64, decimals: i64) -> f64 {
(value * 10_f64.powi(decimals as i32)).round() / 10_f64.powi(decimals as i32)
}
pub fn fill_data(lines: &[String], data: &serde_json::Value) -> Result<Vec<String>, String> {
let parsed_data = evaluate_all_expressions(data)?;
let mut new_lines: Vec<String> = Vec::new();
let mut reg = handlebars::Handlebars::new();
reg.register_helper("upper", Box::new(upper));
reg.register_helper("lower", Box::new(lower));
reg.register_helper("round", Box::new(round_helper));
reg.register_helper("roundup", Box::new(roundup_helper));
reg.register_helper("pm", Box::new(pm_helper));
reg.register_helper("sep", Box::new(sep_helper));
reg.register_helper("pow", Box::new(exponent_helper));
reg.set_strict_mode(true);
for (i, line) in lines.iter().enumerate() {
match reg.render_template(line, &parsed_data) {
Ok(l) => new_lines.push(l),
Err(e) => {
let re = e.as_render_error();
let col = match re {
Some(re2) => re2.column_no.unwrap_or(0_usize),
None => 0_usize,
};
let desc = match re {
Some(re2) => re2.desc.replace(" in strict mode", ""),
None => "Template render error.".into(),
};
let err = format!("WARNING L{}C{}: {}\n", i + 1, col, desc);
std::io::stderr().write_all(err.as_bytes()).unwrap();
new_lines.push(line.to_owned())
}
};
}
Ok(new_lines)
}
fn find_expressions(data: &Json, parent: Option<&Vec<String>>) -> Vec<(Vec<String>, String)> {
let relative_parent: Vec<String> = match parent {
Some(p) => p.to_owned(),
None => Vec::new(),
};
let mut output: Vec<(Vec<String>, String)> = Vec::new();
if let Json::Array(arr) = data {
for val in arr {
let expressions = find_expressions(val, Some(&relative_parent));
for expression in expressions {
output.push(expression);
}
}
};
if let Json::Object(obj) = data {
for (key, val) in obj {
let mut relative_parent2 = relative_parent.to_owned();
relative_parent2.push(key.to_owned());
let expressions = find_expressions(val, Some(&relative_parent2));
for expression in expressions {
output.push(expression);
}
}
};
if let Json::String(string) = data {
if string.trim().starts_with("expr:") {
output.push((relative_parent, string.to_owned()));
}
};
output
}
fn run_eval(expr_string: &str, data: &Json) -> Result<Json, String> {
let mut expr = eval::Expr::new(expr_string);
expr = expr.function("round", |args: Vec<Json>| {
let value = match args.get(0) {
Some(Json::Number(x)) => x.as_f64().unwrap(),
_ => return Err(eval::Error::ExpectedNumber),
};
let decimals = match args.get(1) {
Some(Json::Number(x)) => x.as_f64().unwrap(),
Some(_) => return Err(eval::Error::ExpectedNumber),
None => 0.0,
};
if decimals.fract() > 0.0 {
return Err(eval::Error::Custom(format!(
"Second rounding argument must be an integer. Given value: {}",
decimals
)));
};
let rounded = round_value(value, decimals as i64);
match rounded.fract() == 0.0 {
true => Ok(serde_json::json!(rounded as i64)),
false => Ok(serde_json::json!(rounded)),
}
});
expr = expr.function("pow", |args: Vec<Json>| {
let value = match args.get(0) {
Some(Json::Number(x)) => x.as_f64().unwrap(),
_ => return Err(eval::Error::ExpectedNumber),
};
let exponent = match args.get(1) {
Some(Json::Number(x)) => x.as_f64().unwrap(),
_ => return Err(eval::Error::ExpectedNumber),
};
let product = match exponent >= 0.0 {
true => value.powf(exponent),
false => 1.0 / value.powf(-exponent),
};
match product.fract() == 0.0 {
true => Ok(serde_json::json!(product as i64)),
false => Ok(serde_json::json!(product)),
}
});
expr = expr.function("E", |args: Vec<Json>| {
if args.len() != 1 {
return Err(eval::Error::ArgumentsGreater(1));
};
let exponent = match args.get(0) {
Some(x) => match json_as_float(x) {
Ok(v) => v,
Err(e) => return Err(eval::Error::Custom(e)),
},
_ => return Err(eval::Error::ExpectedNumber),
};
let product = match exponent >= 0.0 {
true => 10_f64.powf(exponent),
false => 1.0 / 10_f64.powf(-exponent),
};
match product.fract() == 0.0 {
true => Ok(serde_json::json!(product as i64)),
false => Ok(serde_json::json!(product)),
}
});
if let Json::Object(obj) = data {
for (key, val) in obj {
expr = expr.value(key, val);
}
};
match expr.exec() {
Err(err) => {
let mut err_str = err.to_string();
if err_str.contains("Null") {
err_str += ". Perhaps a key is misspelled?"
}
Err(format!(
"Error in expression: '{}': {}",
expr_string, err_str
))
}
Ok(Json::Null) => Err(format!("Expression '{}' returned Null value", expr_string)),
Ok(v) => Ok(v),
}
}
fn evaluate_expression(
expression: &str,
data: &Json,
recursion_depth: usize,
) -> Result<Json, String> {
if recursion_depth > 1000 {
return Err(format!(
"Max recursion depth reached for expression: '{}'. Maybe due to a circular expression?",
expression
));
};
let mut expr_string = expression.replacen("expr:", "", 1).trim().to_owned();
let expressions = find_expressions(data, None);
for (keys, expression_str) in &expressions {
if expr_string.contains(&keys.join(".")) {
let value = evaluate_expression(&expression_str, &data, recursion_depth + 1)?;
expr_string = expr_string.replace(&keys.join("."), &value.to_string());
}
}
run_eval(&expr_string, &data)
}
fn replace_value_in_data(data: &mut Json, keys: &[String], value: Json) -> Result<(), String> {
if keys.len() > 1 {
let first_key = &keys[0];
let mut subset = match data.get_mut(first_key) {
Some(s) => s,
None => return Err("Key not found".into()),
};
replace_value_in_data(&mut subset, &keys[1..], value)?;
} else {
match data.get_mut(&keys[0]) {
Some(v) => (*v = value),
None => return Err("Key not found".into()),
};
};
Ok(())
}
fn evaluate_all_expressions(data: &Json) -> Result<Json, String> {
let mut new_data = data.clone();
for (keys, expr_string) in find_expressions(data, None) {
let new_value = match evaluate_expression(&expr_string, &new_data, 0) {
Ok(v) => v,
Err(e) => {
return Err(format!(
"Error for expression in '{}' ('{}'): {:?}",
keys.join("."),
expr_string,
e
))
}
};
match replace_value_in_data(&mut new_data, &keys, new_value) {
Ok(_) => (),
Err(e) => return Err(format!("Error setting key '{}': {}", keys.join("."), e)),
};
}
Ok(new_data)
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn test_fill_data() {
let lines: Vec<String> = vec![
"Hello".into(),
"I am {{years}} years old.".into(),
"Goodbye.".into(),
];
let data = serde_json::json!({"years": 24});
let new_lines = fill_data(&lines, &data).unwrap();
assert_eq!(new_lines[1], "I am 24 years old.");
}
#[test]
fn test_read_data() {
let path = PathBuf::from("tests/data/case2/data.json");
let data = crate::io::read_data(&path).unwrap();
assert_eq!(data.get("year").unwrap(), 2000);
assert_eq!(data.get("year_str").unwrap(), "two thousand");
let lines: Vec<String> = vec![
"The year was once {{year}}".into(),
"This package is called {{package_name}}.".into(),
];
let new_lines = fill_data(&lines, &data).unwrap();
assert_eq!(new_lines[0], "The year was once 2000");
assert_eq!(new_lines[1], "This package is called manus.")
}
#[test]
fn test_round_helpers() {
let lines: Vec<String> = vec![
"Hello".into(),
"{{large_value}} rounded to the nearest 1000 is {{roundup 3 large_value}}".into(),
"{{decimal_value}} rounded to one decimal is {{round 1 decimal_value}}".into(),
];
let data = serde_json::json!({"large_value": 8699, "decimal_value": "1.234"});
let new_lines = fill_data(&lines, &data).unwrap();
assert_eq!(round_value(1.234, 1), 1.2);
assert_eq!(round_value(8699_f64, -3), 9000.0);
assert_eq!(new_lines[0], "Hello");
assert_eq!(new_lines[1], "8699 rounded to the nearest 1000 is 9000");
assert_eq!(new_lines[2], "1.234 rounded to one decimal is 1.2");
}
#[test]
fn test_pm_helper() {
let lines: Vec<String> = vec![
"The value is {{pm data.value}}".into(),
"The value is {{pm 1 data.value}}".into(),
"The other value is {{pm value2}}".into(),
];
let data = serde_json::json!({"data": {"value": 1.2345, "value_pm": 0.2345}, "value2": 2, "value2_pm": 0.1});
let new_lines = fill_data(&lines, &data).unwrap();
assert_eq!(new_lines[0], "The value is 1.2345$\\pm$0.2345");
assert_eq!(new_lines[1], "The value is 1.2$\\pm$0.2");
assert_eq!(new_lines[2], "The other value is 2$\\pm$0.1");
}
#[test]
fn test_sep_helper() {
let lines: Vec<String> = vec![
"10000 is a large number.".into(),
"{{sep 10000}} looks better.".into(),
"{{sep str_with_numerics}}".into(),
"{{sep (pm value)}}".into(),
];
assert_eq!(add_separators(10000., ","), "10,000");
assert_eq!(add_separators(123456.78901, ","), "123,456.78901");
assert_eq!(add_separators(123456., "\\,"), "123\\,456");
let data = serde_json::json!({
"separator": ",",
"str_with_numerics": "Data are 12345 years old with a mean of 1.4858",
"value": -123456789,
"value_pm": 12456
});
let new_lines = fill_data(&lines, &data).unwrap();
assert_eq!(new_lines[0], "10000 is a large number.");
assert_eq!(new_lines[1], "10,000 looks better.");
assert_eq!(
new_lines[2],
"Data are 12,345 years old with a mean of 1.4858"
);
assert_eq!(new_lines[3], "-123,456,789$\\pm$12,456");
}
#[test]
fn test_expressions() {
let lines: Vec<String> = vec![
"The percentage of {{small}} out of {{large}} is {{round percentage}}".into(),
"Adding one percentage point, it becomes: {{round added_percentage}}".into(),
"Ten to the power of three is {{powup}}, and ten to the power of minus two is {{powdown}}".into()
];
let data = serde_json::json!({
"large": 10000,
"small": 200,
"percentage": "expr: 100 * small / large",
"added_percentage": "expr: percentage + 1",
"powup": "expr: pow(10, 3)",
"powdown": "expr: pow(10, -2.0)",
"three": "expr: 1 + 2",
"nested_expressions": {
"value_sum": "expr: large + small",
}
});
assert_eq!(run_eval(&"100 * 3", &data), Ok(serde_json::json!(300)));
assert_eq!(
run_eval("round(1.23, 1)", &data),
Ok(serde_json::json!(1.2))
);
assert_eq!(run_eval("round(1.23)", &data), Ok(serde_json::json!(1)));
match run_eval("round(1.23, 1.2)", &data) {
Ok(v) => panic!("This should have failed!: {:?}", v),
Err(e) => assert!(e.contains("must be an integer")),
}
assert_eq!(run_eval("E(3)", &data), Ok(serde_json::json!(1000)));
assert_eq!(run_eval("E(0-1)", &data), Ok(serde_json::json!(0.1)));
assert_eq!(run_eval("3 * E(0-2)", &data), Ok(serde_json::json!(0.03)));
assert_eq!(run_eval("pow(10, 3)", &data), Ok(serde_json::json!(1000)));
assert_eq!(run_eval("pow(10, 0-1)", &data), Ok(serde_json::json!(0.1)));
assert_eq!(
run_eval("pow(92809.984, 0)", &data),
Ok(serde_json::json!(1))
);
match run_eval(&"largee + small", &data) {
Ok(v) => panic!("This should have failed!: {:?}", v),
Err(e) => assert!(e.contains("Perhaps a key is misspelled?")),
};
println!("{:?}", find_expressions(&data, None));
let parsed_data = evaluate_all_expressions(&data).unwrap();
let new_lines = fill_data(&lines, &parsed_data).unwrap();
assert_eq!(parsed_data["three"], serde_json::json!(3));
assert_eq!(parsed_data["percentage"], serde_json::json!(2.0));
assert_eq!(
parsed_data["nested_expressions"]["value_sum"],
serde_json::json!(10200)
);
assert_eq!(new_lines[0], "The percentage of 200 out of 10000 is 2");
assert_eq!(new_lines[1], "Adding one percentage point, it becomes: 3");
let data = serde_json::json!({
"ex1": "expr: ex2 + 1",
"ex2": "expr: ex1 + 1",
"ex3": "expr: ex3 + 1"
});
assert!(data.is_object());
match evaluate_expression("ex1 + ex2", &data, 0) {
Ok(v) => panic!("This should have failed!: {:?}", v),
Err(s) => assert!(s.contains("recursion"), "{}", s),
};
}
}