botserver/src/basic/keywords/format.rs
Rodrigo Rodriguez (Pragmatismo) 7f1e6dc91c feat: refactor auth and models, update LLM fallback strategy
- Simplified auth module by removing unused imports and code
- Cleaned up shared models by removing unused structs (Organization, User, Bot, etc.)
- Updated add-req.sh to comment out unused directories
- Modified LLM fallback strategy in README with additional notes about model behaviors

The changes focus on removing unused code and improving documentation while maintaining existing functionality. The auth module was significantly reduced by removing redundant code, and similar cleanup was applied to shared models. The build script was adjusted to reflect currently used directories.
2025-11-04 23:11:33 -03:00

304 lines
9.4 KiB
Rust
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use rhai::{Dynamic, Engine};
use chrono::{NaiveDateTime, Timelike, Datelike};
use num_format::{Locale, ToFormattedString};
use std::str::FromStr;
pub fn format_keyword(engine: &mut Engine) {
engine
.register_custom_syntax(&["FORMAT", "$expr$", "$expr$"], false, {
move |context, inputs| {
let value_dyn = context.eval_expression_tree(&inputs[0])?;
let pattern_dyn = context.eval_expression_tree(&inputs[1])?;
let value_str = value_dyn.to_string();
let pattern = pattern_dyn.to_string();
if let Ok(num) = f64::from_str(&value_str) {
let formatted = if pattern.starts_with("N") || pattern.starts_with("C") {
let (prefix, decimals, locale_tag) = parse_pattern(&pattern);
let locale = get_locale(&locale_tag);
let symbol = if prefix == "C" {
get_currency_symbol(&locale_tag)
} else {
""
};
let int_part = num.trunc() as i64;
let frac_part = num.fract();
if decimals == 0 {
format!("{}{}", symbol, int_part.to_formatted_string(&locale))
} else {
let frac_scaled =
((frac_part * 10f64.powi(decimals as i32)).round()) as i64;
let decimal_sep = match locale_tag.as_str() {
"pt" | "fr" | "es" | "it" | "de" => ",",
_ => "."
};
format!(
"{}{}{}{:0width$}",
symbol,
int_part.to_formatted_string(&locale),
decimal_sep,
frac_scaled,
width = decimals
)
}
} else {
match pattern.as_str() {
"n" => format!("{:.2}", num),
"F" => format!("{:.2}", num),
"f" => format!("{}", num),
"0%" => format!("{:.0}%", num * 100.0),
_ => format!("{}", num),
}
};
return Ok(Dynamic::from(formatted));
}
if let Ok(dt) = NaiveDateTime::parse_from_str(&value_str, "%Y-%m-%d %H:%M:%S") {
let formatted = apply_date_format(&dt, &pattern);
return Ok(Dynamic::from(formatted));
}
let formatted = apply_text_placeholders(&value_str, &pattern);
Ok(Dynamic::from(formatted))
}
})
.unwrap();
}
fn parse_pattern(pattern: &str) -> (String, usize, String) {
let mut prefix = String::new();
let mut decimals: usize = 2;
let mut locale_tag = "en".to_string();
if pattern.starts_with('C') {
prefix = "C".to_string();
} else if pattern.starts_with('N') {
prefix = "N".to_string();
}
let rest = &pattern[1..];
let mut num_part = String::new();
for ch in rest.chars() {
if ch.is_ascii_digit() {
num_part.push(ch);
} else {
break;
}
}
if !num_part.is_empty() {
decimals = num_part.parse().unwrap_or(2);
}
if let Some(start) = pattern.find('[') {
if let Some(end) = pattern.find(']') {
if end > start {
locale_tag = pattern[start + 1..end].to_string();
}
}
}
(prefix, decimals, locale_tag)
}
fn get_locale(tag: &str) -> Locale {
match tag {
"en" => Locale::en,
"fr" => Locale::fr,
"de" => Locale::de,
"pt" => Locale::pt,
"it" => Locale::it,
"es" => Locale::es,
_ => Locale::en,
}
}
fn get_currency_symbol(tag: &str) -> &'static str {
match tag {
"en" => "$",
"pt" => "R$ ",
"fr" | "de" | "es" | "it" => "",
_ => "$",
}
}
fn apply_date_format(dt: &NaiveDateTime, pattern: &str) -> String {
let mut output = pattern.to_string();
let year = dt.year();
let month = dt.month();
let day = dt.day();
let hour24 = dt.hour();
let minute = dt.minute();
let second = dt.second();
let millis = dt.and_utc().timestamp_subsec_millis();
output = output.replace("yyyy", &format!("{:04}", year));
output = output.replace("yy", &format!("{:02}", year % 100));
output = output.replace("MM", &format!("{:02}", month));
output = output.replace("M", &format!("{}", month));
output = output.replace("dd", &format!("{:02}", day));
output = output.replace("d", &format!("{}", day));
output = output.replace("HH", &format!("{:02}", hour24));
output = output.replace("H", &format!("{}", hour24));
let mut hour12 = hour24 % 12;
if hour12 == 0 { hour12 = 12; }
output = output.replace("hh", &format!("{:02}", hour12));
output = output.replace("h", &format!("{}", hour12));
output = output.replace("mm", &format!("{:02}", minute));
output = output.replace("m", &format!("{}", minute));
output = output.replace("ss", &format!("{:02}", second));
output = output.replace("s", &format!("{}", second));
output = output.replace("fff", &format!("{:03}", millis));
output = output.replace("tt", if hour24 < 12 { "AM" } else { "PM" });
output = output.replace("t", if hour24 < 12 { "A" } else { "P" });
output
}
fn apply_text_placeholders(value: &str, pattern: &str) -> String {
let mut result = String::new();
let mut i = 0;
let chars: Vec<char> = pattern.chars().collect();
while i < chars.len() {
match chars[i] {
'@' => result.push_str(value),
'&' => {
result.push_str(&value.to_lowercase());
// Handle modifiers
if i + 1 < chars.len() {
match chars[i+1] {
'!' => {
result.push('!');
i += 1;
}
'>' => {
i += 1;
}
_ => ()
}
}
}
'>' | '!' => result.push_str(&value.to_uppercase()),
_ => result.push(chars[i]),
}
i += 1;
}
result
}
#[cfg(test)]
mod tests {
use super::*;
use rhai::Engine;
fn create_engine() -> Engine {
let mut engine = Engine::new();
format_keyword(&mut engine);
engine
}
#[test]
fn test_numeric_formatting_basic() {
let engine = create_engine();
assert_eq!(
engine.eval::<String>("FORMAT 1234.567 \"n\"").unwrap(),
"1234.57"
);
assert_eq!(
engine.eval::<String>("FORMAT 1234.5 \"F\"").unwrap(),
"1234.50"
);
assert_eq!(
engine.eval::<String>("FORMAT 1234.567 \"f\"").unwrap(),
"1234.567"
);
assert_eq!(
engine.eval::<String>("FORMAT 0.85 \"0%\"").unwrap(),
"85%"
);
}
#[test]
fn test_numeric_formatting_with_locale() {
let engine = create_engine();
assert_eq!(
engine.eval::<String>("FORMAT 1234.56 \"N[en]\"").unwrap(),
"1,234.56"
);
assert_eq!(
engine.eval::<String>("FORMAT 1234.56 \"N[pt]\"").unwrap(),
"1.234,56"
);
assert_eq!(
engine.eval::<String>("FORMAT 1234.56 \"N[fr]\"").unwrap(),
"1234,56"
);
}
#[test]
fn test_currency_formatting() {
let engine = create_engine();
assert_eq!(
engine.eval::<String>("FORMAT 1234.56 \"C[en]\"").unwrap(),
"$1,234.56"
);
assert_eq!(
engine.eval::<String>("FORMAT 1234.56 \"C[pt]\"").unwrap(),
"R$ 1.234,56"
);
assert_eq!(
engine.eval::<String>("FORMAT 1234.56 \"C[fr]\"").unwrap(),
"€1234,56"
);
}
#[test]
fn test_date_formatting() {
let engine = create_engine();
let result = engine.eval::<String>("FORMAT \"2024-03-15 14:30:25\" \"yyyy-MM-dd HH:mm:ss\"").unwrap();
assert_eq!(result, "2024-03-15 14:30:25");
let result = engine.eval::<String>("FORMAT \"2024-03-15 14:30:25\" \"dd/MM/yyyy\"").unwrap();
assert_eq!(result, "15/03/2024");
let result = engine.eval::<String>("FORMAT \"2024-03-15 14:30:25\" \"MM/dd/yy\"").unwrap();
assert_eq!(result, "03/15/24");
}
#[test]
fn test_text_formatting() {
let engine = create_engine();
assert_eq!(
engine.eval::<String>("FORMAT \"hello\" \"Prefix: @\"").unwrap(),
"Prefix: hello"
);
assert_eq!(
engine.eval::<String>("FORMAT \"HELLO\" \"Result: &!\"").unwrap(),
"Result: hello!"
);
assert_eq!(
engine.eval::<String>("FORMAT \"hello\" \"RESULT: >\"").unwrap(),
"RESULT: HELLO"
);
}
}