use async_trait::async_trait;
use crate::ast::Value;
use crate::interpreter::{ExecResult, OutputData, OutputNode};
use crate::tools::{ExecContext, ParamSchema, Tool, ToolArgs, ToolSchema, validate_against_schema};
use crate::validator::{IssueCode, ValidationIssue};
pub struct Seq;
#[async_trait]
impl Tool for Seq {
fn name(&self) -> &str {
"seq"
}
fn schema(&self) -> ToolSchema {
ToolSchema::new("seq", "Print sequences of numbers")
.param(ParamSchema::required(
"last",
"number",
"Last number (or first if two args)",
))
.param(ParamSchema::optional(
"first",
"number",
Value::Int(1),
"First number (default 1)",
))
.param(ParamSchema::optional(
"increment",
"number",
Value::Int(1),
"Increment (default 1)",
))
.param(ParamSchema::optional(
"separator",
"string",
Value::String("\n".into()),
"Separator between numbers (-s)",
).with_aliases(["-s"]))
.param(ParamSchema::optional(
"width",
"bool",
Value::Bool(false),
"Equalize width by padding with zeros (-w)",
).with_aliases(["-w"]))
.example("Count to 5", "seq 5")
.example("Range with step", "seq 1 2 10")
.example("Zero-padded", "seq -w 1 100")
}
fn validate(&self, args: &ToolArgs) -> Vec<ValidationIssue> {
let mut issues = validate_against_schema(args, &self.schema());
if args.positional.len() == 3 {
if let Some(Value::Int(0)) = args.positional.get(1) {
issues.push(ValidationIssue::error(
IssueCode::SeqZeroIncrement,
"seq: increment cannot be zero (would cause infinite loop)",
));
} else if let Some(Value::Float(f)) = args.positional.get(1) {
if *f == 0.0 {
issues.push(ValidationIssue::error(
IssueCode::SeqZeroIncrement,
"seq: increment cannot be zero (would cause infinite loop)",
));
}
} else if let Some(Value::String(s)) = args.positional.get(1)
&& let Ok(n) = s.parse::<f64>()
&& n == 0.0 {
issues.push(ValidationIssue::error(
IssueCode::SeqZeroIncrement,
"seq: increment cannot be zero (would cause infinite loop)",
));
}
}
issues
}
async fn execute(&self, args: ToolArgs, _ctx: &mut ExecContext) -> ExecResult {
let (first, increment, last) = match (
args.get_positional(0),
args.get_positional(1),
args.get_positional(2),
) {
(Some(v1), None, None) => {
let last = value_to_f64(v1);
(1.0, 1.0, last)
}
(Some(v1), Some(v2), None) => {
let first = value_to_f64(v1);
let last = value_to_f64(v2);
(first, 1.0, last)
}
(Some(v1), Some(v2), Some(v3)) => {
let first = value_to_f64(v1);
let increment = value_to_f64(v2);
let last = value_to_f64(v3);
(first, increment, last)
}
_ => return ExecResult::failure(1, "seq: missing argument"),
};
if increment == 0.0 {
return ExecResult::failure(1, "seq: increment cannot be zero");
}
let separator = args
.get_string("separator", usize::MAX)
.or_else(|| args.get_string("s", usize::MAX))
.unwrap_or_else(|| "\n".to_string());
let pad_width = args.has_flag("width") || args.has_flag("w");
let mut numbers = Vec::new();
let mut current = first;
let tolerance = increment.abs() * 1e-10;
if increment > 0.0 {
while current <= last + tolerance {
numbers.push(current);
current += increment;
}
} else {
while current >= last - tolerance {
numbers.push(current);
current += increment;
}
}
if numbers.is_empty() {
return ExecResult::with_output(OutputData::new());
}
let is_integer = numbers.iter().all(|n| n.fract().abs() < f64::EPSILON);
let formatted: Vec<String> = if is_integer {
let max_width = if pad_width {
numbers
.iter()
.map(|n| (*n as i64).abs().to_string().len())
.max()
.unwrap_or(1)
} else {
0
};
numbers
.iter()
.map(|n| {
let i = *n as i64;
if pad_width {
format!("{:0>width$}", i, width = max_width)
} else {
i.to_string()
}
})
.collect()
} else {
numbers.iter().map(|n| format!("{}", n)).collect()
};
let nodes: Vec<OutputNode> = formatted
.iter()
.map(|s| OutputNode::new(s.as_str()))
.collect();
let json_array: Vec<serde_json::Value> = numbers
.iter()
.map(|n| {
if is_integer {
serde_json::Value::Number((*n as i64).into())
} else {
serde_json::Number::from_f64(*n)
.map(serde_json::Value::Number)
.unwrap_or(serde_json::Value::Null)
}
})
.collect();
let output_data = OutputData::nodes(nodes);
let mut result = ExecResult::with_output_and_text(output_data, formatted.join(&separator) + "\n");
result.data = Some(Value::Json(serde_json::Value::Array(json_array)));
result
}
}
fn value_to_f64(v: &Value) -> f64 {
match v {
Value::Int(i) => *i as f64,
Value::Float(f) => *f,
Value::String(s) => s.parse().unwrap_or(0.0),
_ => 0.0,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::vfs::{MemoryFs, VfsRouter};
use std::sync::Arc;
fn make_ctx() -> ExecContext {
let mut vfs = VfsRouter::new();
vfs.mount("/", MemoryFs::new());
ExecContext::new(Arc::new(vfs))
}
#[tokio::test]
async fn test_seq_single_arg() {
let mut ctx = make_ctx();
let mut args = ToolArgs::new();
args.positional.push(Value::Int(5));
let result = Seq.execute(args, &mut ctx).await;
assert!(result.ok());
let text = result.text_out();
let lines: Vec<&str> = text.lines().collect();
assert_eq!(lines, vec!["1", "2", "3", "4", "5"]);
}
#[tokio::test]
async fn test_seq_first_last() {
let mut ctx = make_ctx();
let mut args = ToolArgs::new();
args.positional.push(Value::Int(3));
args.positional.push(Value::Int(7));
let result = Seq.execute(args, &mut ctx).await;
assert!(result.ok());
let text = result.text_out();
let lines: Vec<&str> = text.lines().collect();
assert_eq!(lines, vec!["3", "4", "5", "6", "7"]);
}
#[tokio::test]
async fn test_seq_with_increment() {
let mut ctx = make_ctx();
let mut args = ToolArgs::new();
args.positional.push(Value::Int(1));
args.positional.push(Value::Int(2));
args.positional.push(Value::Int(10));
let result = Seq.execute(args, &mut ctx).await;
assert!(result.ok());
let text = result.text_out();
let lines: Vec<&str> = text.lines().collect();
assert_eq!(lines, vec!["1", "3", "5", "7", "9"]);
}
#[tokio::test]
async fn test_seq_negative_increment() {
let mut ctx = make_ctx();
let mut args = ToolArgs::new();
args.positional.push(Value::Int(5));
args.positional.push(Value::Int(-1));
args.positional.push(Value::Int(1));
let result = Seq.execute(args, &mut ctx).await;
assert!(result.ok());
let text = result.text_out();
let lines: Vec<&str> = text.lines().collect();
assert_eq!(lines, vec!["5", "4", "3", "2", "1"]);
}
#[tokio::test]
async fn test_seq_custom_separator() {
let mut ctx = make_ctx();
let mut args = ToolArgs::new();
args.positional.push(Value::Int(3));
args.named
.insert("separator".to_string(), Value::String(", ".into()));
let result = Seq.execute(args, &mut ctx).await;
assert!(result.ok());
assert_eq!(result.text_out().trim(), "1, 2, 3");
}
#[tokio::test]
async fn test_seq_zero_pad() {
let mut ctx = make_ctx();
let mut args = ToolArgs::new();
args.positional.push(Value::Int(10));
args.flags.insert("w".to_string());
let result = Seq.execute(args, &mut ctx).await;
assert!(result.ok());
let text = result.text_out();
let lines: Vec<&str> = text.lines().collect();
assert_eq!(lines[0], "01");
assert_eq!(lines[9], "10");
}
#[tokio::test]
async fn test_seq_float() {
let mut ctx = make_ctx();
let mut args = ToolArgs::new();
args.positional.push(Value::Float(1.0));
args.positional.push(Value::Float(0.5));
args.positional.push(Value::Float(2.0));
let result = Seq.execute(args, &mut ctx).await;
assert!(result.ok());
let text = result.text_out();
let lines: Vec<&str> = text.lines().collect();
assert_eq!(lines.len(), 3); }
#[tokio::test]
async fn test_seq_float_boundary() {
let mut ctx = make_ctx();
let mut args = ToolArgs::new();
args.positional.push(Value::Float(0.1));
args.positional.push(Value::Float(0.1));
args.positional.push(Value::Float(1.0));
let result = Seq.execute(args, &mut ctx).await;
assert!(result.ok());
let text = result.text_out();
let lines: Vec<&str> = text.lines().collect();
assert_eq!(lines.len(), 10, "expected 10 values, got {}: {:?}", lines.len(), lines);
}
#[tokio::test]
async fn test_seq_zero_increment() {
let mut ctx = make_ctx();
let mut args = ToolArgs::new();
args.positional.push(Value::Int(1));
args.positional.push(Value::Int(0));
args.positional.push(Value::Int(5));
let result = Seq.execute(args, &mut ctx).await;
assert!(!result.ok());
assert!(result.err.contains("zero"));
}
}