use crate::value::Value;
pub struct TokenEstimator;
impl TokenEstimator {
pub fn estimate(value: &Value) -> usize {
let chars = Self::count_characters(value);
(chars + 3) / 4
}
pub fn estimate_breakdown(value: &Value) -> TokenBreakdown {
let mut breakdown = TokenBreakdown::new();
Self::analyze_value(value, &mut breakdown);
breakdown
}
fn count_characters(value: &Value) -> usize {
match value {
Value::Null => 4,
Value::Bool(b) => if *b { 4 } else { 5 },
Value::Number(n) => n.to_string().len(),
Value::String(s) => s.len() + 2, Value::Array(arr) => {
let mut total = 2; for (i, item) in arr.iter().enumerate() {
if i > 0 {
total += 2; }
total += Self::count_characters(item);
}
total
}
Value::Object(obj) => {
let mut total = 2; for (i, (key, val)) in obj.iter().enumerate() {
if i > 0 {
total += 2; }
total += key.len() + 3; total += Self::count_characters(val);
}
total
}
Value::Table(table) => {
let mut total = table.name.len() + 10; for row in &table.rows {
total += 2; for (key, val) in row {
total += key.len() + 2; total += Self::count_characters(val);
}
}
total
}
}
}
fn analyze_value(value: &Value, breakdown: &mut TokenBreakdown) {
match value {
Value::Null => breakdown.add_primitive(1),
Value::Bool(_) => breakdown.add_primitive(1),
Value::Number(_) => breakdown.add_primitive(1),
Value::String(s) => breakdown.add_string((s.len() + 3) / 4),
Value::Array(arr) => {
breakdown.add_structure(1);
for item in arr {
Self::analyze_value(item, breakdown);
}
}
Value::Object(obj) => {
breakdown.add_structure(obj.len());
for (key, val) in obj {
breakdown.add_key((key.len() + 3) / 4);
Self::analyze_value(val, breakdown);
}
}
Value::Table(table) => {
breakdown.add_table(
(table.name.len() + 3) / 4,
table.rows.len()
);
for row in &table.rows {
for (key, val) in row {
breakdown.add_key((key.len() + 3) / 4);
Self::analyze_value(val, breakdown);
}
}
}
}
}
}
#[derive(Debug, Clone)]
pub struct TokenBreakdown {
pub keys: usize,
pub strings: usize,
pub primitives: usize,
pub structure: usize,
pub tables: usize,
pub table_rows: usize,
}
impl TokenBreakdown {
pub fn new() -> Self {
TokenBreakdown {
keys: 0,
strings: 0,
primitives: 0,
structure: 0,
tables: 0,
table_rows: 0,
}
}
pub fn add_key(&mut self, tokens: usize) {
self.keys += tokens;
}
pub fn add_string(&mut self, tokens: usize) {
self.strings += tokens;
}
pub fn add_primitive(&mut self, tokens: usize) {
self.primitives += tokens;
}
pub fn add_structure(&mut self, tokens: usize) {
self.structure += tokens;
}
pub fn add_table(&mut self, name_tokens: usize, rows: usize) {
self.tables += name_tokens;
self.table_rows += rows;
}
pub fn total(&self) -> usize {
self.keys + self.strings + self.primitives + self.structure + self.tables
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::value::Table;
use std::collections::HashMap;
#[test]
fn test_estimate_primitives() {
assert_eq!(TokenEstimator::estimate(&Value::Null), 1);
assert_eq!(TokenEstimator::estimate(&Value::Bool(true)), 1);
assert_eq!(TokenEstimator::estimate(&Value::Bool(false)), 2);
assert_eq!(TokenEstimator::estimate(&Value::Number(42.0)), 1);
assert_eq!(TokenEstimator::estimate(&Value::Number(12345.0)), 2);
}
#[test]
fn test_estimate_string() {
assert_eq!(TokenEstimator::estimate(&Value::String("test".to_string())), 2);
assert_eq!(TokenEstimator::estimate(&Value::String("a longer string".to_string())), 5);
}
#[test]
fn test_estimate_array() {
let arr = Value::Array(vec![
Value::Number(1.0),
Value::Number(2.0),
Value::Number(3.0),
]);
assert_eq!(TokenEstimator::estimate(&arr), 3);
}
#[test]
fn test_estimate_object() {
let mut obj = HashMap::new();
obj.insert("name".to_string(), Value::String("Alice".to_string()));
obj.insert("age".to_string(), Value::Number(30.0));
let value = Value::Object(obj);
let estimate = TokenEstimator::estimate(&value);
assert!(estimate > 0);
assert!(estimate < 20); }
#[test]
fn test_breakdown_simple() {
let mut obj = HashMap::new();
obj.insert("name".to_string(), Value::String("test".to_string()));
obj.insert("count".to_string(), Value::Number(5.0));
obj.insert("active".to_string(), Value::Bool(true));
let value = Value::Object(obj);
let breakdown = TokenEstimator::estimate_breakdown(&value);
assert_eq!(breakdown.keys, 5); assert_eq!(breakdown.strings, 1); assert_eq!(breakdown.primitives, 2); assert_eq!(breakdown.structure, 3); assert_eq!(breakdown.tables, 0);
assert_eq!(breakdown.table_rows, 0);
}
#[test]
fn test_breakdown_with_table() {
let mut row1 = HashMap::new();
row1.insert("id".to_string(), Value::Number(1.0));
row1.insert("name".to_string(), Value::String("Alice".to_string()));
let mut row2 = HashMap::new();
row2.insert("id".to_string(), Value::Number(2.0));
row2.insert("name".to_string(), Value::String("Bob".to_string()));
let table = Table {
name: "users".to_string(),
declared_rows: 2,
rows: vec![row1, row2],
};
let mut obj = HashMap::new();
obj.insert("users".to_string(), Value::Table(table));
let value = Value::Object(obj);
let breakdown = TokenEstimator::estimate_breakdown(&value);
assert!(breakdown.keys > 0); assert!(breakdown.strings > 0); assert!(breakdown.primitives > 0); assert_eq!(breakdown.tables, 2); assert_eq!(breakdown.table_rows, 2); }
#[test]
fn test_breakdown_total() {
let breakdown = TokenBreakdown {
keys: 10,
strings: 5,
primitives: 3,
structure: 2,
tables: 1,
table_rows: 0,
};
assert_eq!(breakdown.total(), 21);
}
}