Time Series Trend Detector (java, written by Codex)
envgap__codex__java-t1-9
Written by a coding agent; not on GitHubWritten 2026-03-02
01 / FAILURE SIGNATURE
As the study recorded it
Compilation failure: local variable referenced from lambda must be final or effectively final
Not a benchmark task.
- Its repair changed source code, so it is not an environment task.
02 / ENVIRONMENT RECIPE
- Base commit
Not freshly verified- Manifest
pom.xml- Reproduce
Awaiting issue-specific recipe- Run under trace
Awaiting a meaningful runtime command
03 / TASK AND FAILURE
codex/java-t1 #9 · read the task the agent was given
Codex wrote this java project from the task below. It does not run on a clean Ubuntu 22.04 machine as written. Task given to the agent: TASK: Time Series Trend Detector Write a program that analyzes time series data to detect trends, seasonal patterns, and anomalies using statistical methods, and produces a visual summary report. FUNCTIONAL REQUIREMENTS: - Accept a CSV file path as a command-line argument with columns for timestamp and one or more numeric value columns - Parse timestamps in multiple formats (ISO 8601, Unix epoch, and common date formats like MM/DD/YYYY, YYYY-MM-DD HH:MM:SS) - Compute a moving average with a configurable window size via --window flag (default: 7 data points) - Detect overall trend direction (increasing, decreasing, stable) using linear regression and report the slope and R-squared value - Detect seasonality by computing autocorrelation at various lags and reporting the dominant period if one exists - Identify anomalies: data points that deviate more than a configurable number of standard deviations from the moving average (--threshold flag, default: 2.0) - Support multiple value columns: analyze each independently and report results for all - Generate a summary report with: trend direction and strength, seasonal period (if any), count and list of anomalies with their timestamps and values, basic statistics (min, max, mean, variance) - Save the report as a JSON file with --output flag (default: trend_report.json) - Export the processed data (original values, moving average, anomaly flags) as a CSV file via --export flag - If no input file is given, generate a sample time series dataset with 365 daily data points containing a linear trend, weekly seasonality, and injected anomalies, then analyze it - Handle missing timestamps and gaps in the series by interpolating or flagging them Create a complete Java project for a clean Ubuntu 22.04 machine with only JDK 17+ installed. Include: - Source code - pom.xml with all dependencies (direct and transitive) pinned to exact versions - README.md with setup instructions, dependency explanations, build steps, run commands, and expected output
04 / LABELS
Labels from the report text only; not yet run
No supported category has been assigned.
Label rules and the text that matched
[]
05 / FILES
The project as the agent wrote it
3 files, exactly as written, before any repair.
pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>tmlr.codex_generated.p09</groupId>
<artifactId>time-series-trend-detector</artifactId>
<version>1.0.0</version>
<name>Time Series Trend Detector</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.release>17</maven.compiler.release>
</properties>
<dependencies>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.5.0</version>
<configuration>
<mainClass>TimeSeriesTrendDetector</mainClass>
</configuration>
</plugin>
</plugins>
</build>
</project>
README.md
# Time Series Trend Detector (Java) Analyzes CSV time series data for trend, seasonality, anomalies, and summary statistics. ## Requirements - Ubuntu 22.04 - JDK 17+ - Maven 3.9+ ## Dependencies - Direct: none (Java standard library only) - Transitive: none Build plugin versions are pinned in `pom.xml` for reproducibility. ## Build ```bash mvn -q -DskipTests compile ``` ## Run With input: ```bash mvn -q exec:java -Dexec.args="/path/to/data.csv --window 7 --threshold 2.0 --output trend_report.json --export processed.csv" ``` No input (generates a 365-day sample and analyzes it): ```bash mvn -q exec:java ``` ## Features - Timestamp parsing: ISO 8601, Unix epoch, `MM/DD/YYYY`, `YYYY-MM-DD HH:MM:SS` - Moving average with configurable window - Linear regression trend direction + slope + R-squared - Autocorrelation-based seasonality period detection - Anomaly detection against moving average using configurable std-dev threshold - Gap interpolation and reporting - JSON report and processed CSV export
src/main/java/TimeSeriesTrendDetector.java
import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public class TimeSeriesTrendDetector {
private record ParsedArgs(Map<String, String> options, List<String> positional) {}
private record Row(long timestampMs, String isoTimestamp, Map<String, Double> values, boolean gapFilled) {}
private record Regression(double slope, double intercept, double r2) {}
private record Seasonality(Integer period, Double autocorrelation) {}
private record Analysis(
String column,
Map<String, Double> stats,
Map<String, Object> trend,
Map<String, Object> seasonality,
List<Map<String, Object>> anomalies,
List<Double> movingAverage
) {}
public static void main(String[] args) {
try {
ParsedArgs parsed = parseArgs(args);
int window = Math.max(1, Integer.parseInt(parsed.options.getOrDefault("window", "7")));
double threshold = Double.parseDouble(parsed.options.getOrDefault("threshold", "2.0"));
Path outputPath = Paths.get(parsed.options.getOrDefault("output", "trend_report.json")).toAbsolutePath();
Path exportPath = parsed.options.containsKey("export")
? Paths.get(parsed.options.get("export")).toAbsolutePath()
: null;
Path inputPath;
if (parsed.positional.isEmpty()) {
inputPath = Paths.get("sample_timeseries.csv").toAbsolutePath();
generateSample(inputPath);
System.out.println("No input provided. Generated sample dataset: " + inputPath);
} else {
inputPath = Paths.get(parsed.positional.get(0)).toAbsolutePath();
if (!Files.exists(inputPath)) {
System.err.println("Input file not found: " + inputPath);
System.exit(1);
return;
}
}
ParseResult parsedInput = parseInputCsv(inputPath);
if (parsedInput.rows.isEmpty() || parsedInput.valueColumns.isEmpty()) {
System.err.println("No valid rows or value columns found.");
System.exit(1);
return;
}
GapResult gapResult = interpolateGaps(parsedInput.rows, parsedInput.valueColumns);
List<Analysis> analyses = new ArrayList<>();
for (String col : parsedInput.valueColumns) {
analyses.add(analyzeColumn(gapResult.rows, col, window, threshold));
}
Map<String, Object> report = buildReport(inputPath, parsedInput.timestampColumn, parsedInput.valueColumns,
gapResult, analyses, window, threshold);
Files.writeString(outputPath, toJson(report, 0) + "\n", StandardCharsets.UTF_8);
if (exportPath != null) exportProcessedCsv(exportPath, gapResult.rows, analyses);
printSummary(analyses, gapResult.gapPoints.size(), outputPath, exportPath);
} catch (Exception e) {
System.err.println("Failed: " + e.getMessage());
System.exit(1);
}
}
private record ParseResult(List<Row> rows, List<String> valueColumns, String timestampColumn) {}
private record GapResult(List<Row> rows, long inferredStepMs, List<Long> gapPoints) {}
private static ParsedArgs parseArgs(String[] args) {
Map<String, String> options = new HashMap<>();
List<String> positional = new ArrayList<>();
for (int i = 0; i < args.length; i++) {
String token = args[i];
if (token.startsWith("--")) {
String key = token.substring(2);
if (i + 1 < args.length && !args[i + 1].startsWith("--")) options.put(key, args[++i]);
else options.put(key, "true");
} else {
positional.add(token);
}
}
return new ParsedArgs(options, positional);
}
private static List<String> parseCsvLine(String line) {
List<String> out = new ArrayList<>();
StringBuilder current = new StringBuilder();
boolean inQuotes = false;
for (int i = 0; i < line.length(); i++) {
char ch = line.charAt(i);
if (ch == '"') {
if (inQuotes && i + 1 < line.length() && line.charAt(i + 1) == '"') {
current.append('"');
i++;
} else {
inQuotes = !inQuotes;
}
} else if (ch == ',' && !inQuotes) {
out.add(current.toString());
current.setLength(0);
} else {
current.append(ch);
}
}
out.add(current.toString());
return out;
}
private static Long parseTimestamp(String raw) {
if (raw == null) return null;
String s = raw.trim();
if (s.isEmpty()) return null;
if (s.matches("^\\d{10}$")) return Long.parseLong(s) * 1000L;
if (s.matches("^\\d{13}$")) return Long.parseLong(s);
if (s.matches("^\\d{1,2}/\\d{1,2}/\\d{4}$")) {
DateTimeFormatter f = DateTimeFormatter.ofPattern("M/d/yyyy");
LocalDate d = LocalDate.parse(s, f);
return d.atStartOfDay().toInstant(ZoneOffset.UTC).toEpochMilli();
}
if (s.matches("^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}$")) {
DateTimeFormatter f = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime dt = LocalDateTime.parse(s, f);
return dt.toInstant(ZoneOffset.UTC).toEpochMilli();
}
try {
return Instant.parse(s).toEpochMilli();
} catch (DateTimeParseException ignored) {
}
try {
return LocalDate.parse(s, DateTimeFormatter.ISO_LOCAL_DATE).atStartOfDay().toInstant(ZoneOffset.UTC).toEpochMilli();
} catch (DateTimeParseException ignored) {
}
try {
return LocalDateTime.parse(s, DateTimeFormatter.ISO_LOCAL_DATE_TIME).toInstant(ZoneOffset.UTC).toEpochMilli();
} catch (DateTimeParseException ignored) {
}
return null;
}
private static String iso(long tsMs) {
return Instant.ofEpochMilli(tsMs).toString();
}
private static ParseResult parseInputCsv(Path path) throws IOException {
List<String> lines = Files.readAllLines(path, StandardCharsets.UTF_8);
lines.removeIf(s -> s.trim().isEmpty());
if (lines.isEmpty()) return new ParseResult(new ArrayList<>(), new ArrayList<>(), "timestamp");
if (!lines.get(0).isEmpty() && lines.get(0).charAt(0) == '\uFEFF') {
lines.set(0, lines.get(0).substring(1));
}
List<String> headers = parseCsvLine(lines.get(0));
if (headers.size() < 2) return new ParseResult(new ArrayList<>(), new ArrayList<>(), "timestamp");
String tsCol = headers.get(0);
List<String> valueCols = headers.subList(1, headers.size());
List<Row> rows = new ArrayList<>();
for (int i = 1; i < lines.size(); i++) {
List<String> parts = parseCsvLine(lines.get(i));
if (parts.isEmpty()) continue;
Long ts = parseTimestamp(parts.get(0));
if (ts == null) continue;
Map<String, Double> values = new LinkedHashMap<>();
for (int j = 0; j < valueCols.size(); j++) {
String raw = j + 1 < parts.size() ? parts.get(j + 1).trim() : "";
Double v = null;
if (!raw.isEmpty()) {
try { v = Double.parseDouble(raw); } catch (NumberFormatException ignored) {}
}
values.put(valueCols.get(j), v);
}
rows.add(new Row(ts, iso(ts), values, false));
}
return new ParseResult(rows, new ArrayList<>(valueCols), tsCol);
}
private static void generateSample(Path path) throws IOException {
try (BufferedWriter writer = Files.newBufferedWriter(path, StandardCharsets.UTF_8)) {
writer.write("timestamp,metric_a,metric_b\n");
Instant start = Instant.parse("2025-01-01T00:00:00Z");
for (int i = 0; i < 365; i++) {
Instant ts = start.plusSeconds(86400L * i);
double weekly = 10.0 * Math.sin((2.0 * Math.PI * i) / 7.0);
double trend = i * 0.18;
double a = 50 + trend + weekly + Math.sin(i) * 0.8;
double b = 30 + i * 0.05 + 5 * Math.cos((2.0 * Math.PI * i) / 7.0);
if (i == 45 || i == 123 || i == 251) a += 35;
if (i == 200 || i == 300) b -= 20;
writer.write(String.format("%s,%.3f,%.3f%n", ts.toString().substring(0, 10), a, b));
}
}
}
private static long inferStepMs(List<Row> rows) {
List<Long> diffs = new ArrayList<>();
for (int i = 1; i < rows.size(); i++) {
long d = rows.get(i).timestampMs - rows.get(i - 1).timestampMs;
if (d > 0) diffs.add(d);
}
if (diffs.isEmpty()) return 86400000L;
Collections.sort(diffs);
return diffs.get(diffs.size() / 2);
}
private static GapResult interpolateGaps(List<Row> rows, List<String> valueCols) {
List<Row> ordered = new ArrayList<>(rows);
ordered.sort((a, b) -> Long.compare(a.timestampMs, b.timestampMs));
long step = inferStepMs(ordered);
Map<Long, Row> byTs = new HashMap<>();
for (Row r : ordered) byTs.put(r.timestampMs, r);
long start = ordered.get(0).timestampMs;
long end = ordered.get(ordered.size() - 1).timestampMs;
List<Row> filled = new ArrayList<>();
List<Long> gapPoints = new ArrayList<>();
for (long t = start; t <= end; t += step) {
if (byTs.containsKey(t)) {
Row src = byTs.get(t);
filled.add(new Row(src.timestampMs, src.isoTimestamp, new LinkedHashMap<>(src.values), false));
} else {
Map<String, Double> vals = new LinkedHashMap<>();
for (String c : valueCols) vals.put(c, null);
filled.add(new Row(t, iso(t), vals, true));
gapPoints.add(t);
}
}
for (String col : valueCols) {
for (int i = 0; i < filled.size(); i++) {
if (filled.get(i).values.get(col) != null) continue;
int left = i - 1;
while (left >= 0 && filled.get(left).values.get(col) == null) left--;
int right = i + 1;
while (right < filled.size() && filled.get(right).values.get(col) == null) right++;
if (left >= 0 && right < filled.size()) {
double lv = filled.get(left).values.get(col);
double rv = filled.get(right).values.get(col);
double ratio = (double) (filled.get(i).timestampMs - filled.get(left).timestampMs) /
(double) (filled.get(right).timestampMs - filled.get(left).timestampMs);
double v = lv + (rv - lv) * ratio;
filled.get(i).values.put(col, v);
}
}
}
return new GapResult(filled, step, gapPoints);
}
private static Double mean(List<Double> values) {
if (values.isEmpty()) return null;
double sum = 0.0;
for (Double v : values) sum += v;
return sum / values.size();
}
private static Double variance(List<Double> values, Double m) {
if (values.isEmpty()) return null;
double mu = (m == null ? mean(values) : m);
if (mu == 0 && m == null && values.isEmpty()) return null;
double acc = 0.0;
for (Double v : values) acc += (v - mu) * (v - mu);
return acc / values.size();
}
private static Double stddev(List<Double> values, Double m) {
Double v = variance(values, m);
return v == null ? null : Math.sqrt(v);
}
private static List<Double> movingAverage(List<Double> values, int window) {
List<Double> out = new ArrayList<>(Collections.nCopies(values.size(), null));
for (int i = 0; i < values.size(); i++) {
int start = Math.max(0, i - window + 1);
List<Double> chunk = new ArrayList<>();
for (int j = start; j <= i; j++) if (values.get(j) != null) chunk.add(values.get(j));
out.set(i, chunk.isEmpty() ? null : mean(chunk));
}
return out;
}
private static Regression linearRegression(List<Double> values) {
List<Integer> xs = new ArrayList<>();
List<Double> ys = new ArrayList<>();
for (int i = 0; i < values.size(); i++) {
if (values.get(i) != null) {
xs.add(i);
ys.add(values.get(i));
}
}
if (ys.size() < 2) return new Regression(0.0, 0.0, 0.0);
double mx = mean(xs.stream().map(Double::valueOf).toList());
double my = mean(ys);
double cov = 0.0;
double vx = 0.0;
for (int i = 0; i < ys.size(); i++) {
cov += (xs.get(i) - mx) * (ys.get(i) - my);
vx += (xs.get(i) - mx) * (xs.get(i) - mx);
}
double slope = vx == 0 ? 0 : cov / vx;
double intercept = my - slope * mx;
double ssRes = 0.0;
double ssTot = 0.0;
for (int i = 0; i < ys.size(); i++) {
double pred = slope * xs.get(i) + intercept;
ssRes += (ys.get(i) - pred) * (ys.get(i) - pred);
ssTot += (ys.get(i) - my) * (ys.get(i) - my);
}
double r2 = ssTot == 0 ? 0 : 1.0 - (ssRes / ssTot);
return new Regression(slope, intercept, r2);
}
private static Double autocorrelation(List<Double> values, int lag) {
List<Double> xs = new ArrayList<>();
List<Double> ys = new ArrayList<>();
for (int i = lag; i < values.size(); i++) {
Double a = values.get(i);
Double b = values.get(i - lag);
if (a == null || b == null) continue;
xs.add(a);
ys.add(b);
}
if (xs.size() < 3) return null;
double mx = mean(xs);
double my = mean(ys);
double num = 0.0;
double dx = 0.0;
double dy = 0.0;
for (int i = 0; i < xs.size(); i++) {
num += (xs.get(i) - mx) * (ys.get(i) - my);
dx += (xs.get(i) - mx) * (xs.get(i) - mx);
dy += (ys.get(i) - my) * (ys.get(i) - my);
}
double den = Math.sqrt(dx * dy);
return den == 0 ? 0.0 : num / den;
}
private static Seasonality detectSeasonality(List<Double> values) {
int maxLag = Math.min(60, values.size() / 2);
Integer bestLag = null;
double bestCorr = 0.0;
for (int lag = 2; lag <= maxLag; lag++) {
Double corr = autocorrelation(values, lag);
if (corr == null) continue;
if (Math.abs(corr) > Math.abs(bestCorr)) {
bestCorr = corr;
bestLag = lag;
}
}
if (bestLag == null || Math.abs(bestCorr) < 0.3) return new Seasonality(null, null);
return new Seasonality(bestLag, bestCorr);
}
private static Analysis analyzeColumn(List<Row> rows, String col, int window, double threshold) {
List<Double> values = new ArrayList<>();
for (Row r : rows) values.add(r.values.get(col));
List<Double> ma = movingAverage(values, window);
List<Double> valid = values.stream().filter(v -> v != null).toList();
Double mu = mean(valid);
Double var = variance(valid, mu);
Double sd = stddev(valid, mu);
Regression reg = linearRegression(values);
Seasonality season = detectSeasonality(values);
String direction = "stable";
double scale = (sd == null || sd == 0.0) ? 1.0 : sd;
if (Math.abs(reg.slope) >= scale * 0.001) direction = reg.slope > 0 ? "increasing" : "decreasing";
List<Map<String, Object>> anomalies = new ArrayList<>();
for (int i = 0; i < values.size(); i++) {
Double v = values.get(i);
Double m = ma.get(i);
if (v == null || m == null || sd == null || sd == 0.0) continue;
double z = Math.abs(v - m) / sd;
if (z > threshold) {
Map<String, Object> an = new LinkedHashMap<>();
an.put("index", i);
an.put("timestamp", rows.get(i).isoTimestamp);
an.put("value", v);
an.put("moving_average", m);
an.put("z_from_moving_average", z);
anomalies.add(an);
}
}
Map<String, Double> stats = new LinkedHashMap<>();
stats.put("min", valid.isEmpty() ? null : Collections.min(valid));
stats.put("max", valid.isEmpty() ? null : Collections.max(valid));
stats.put("mean", mu);
stats.put("variance", var);
stats.put("stddev", sd);
Map<String, Object> trend = new LinkedHashMap<>();
trend.put("direction", direction);
trend.put("slope", reg.slope);
trend.put("r_squared", reg.r2);
Map<String, Object> seasonality = new LinkedHashMap<>();
seasonality.put("period", season.period);
seasonality.put("autocorrelation", season.autocorrelation);
return new Analysis(col, stats, trend, seasonality, anomalies, ma);
}
private static Map<String, Object> buildReport(Path inputPath, String timestampColumn, List<String> valueColumns,
GapResult gapResult, List<Analysis> analyses, int window, double threshold) {
Map<String, Object> metadata = new LinkedHashMap<>();
metadata.put("input_file", inputPath.toString());
metadata.put("generated_at", Instant.now().toString());
metadata.put("window", window);
metadata.put("threshold", threshold);
metadata.put("inferred_step_millis", gapResult.inferredStepMs);
List<String> gapIso = gapResult.gapPoints.stream().map(TimeSeriesTrendDetector::iso).toList();
metadata.put("interpolated_gap_points", gapIso);
List<Map<String, Object>> analysisOut = new ArrayList<>();
for (Analysis a : analyses) {
Map<String, Object> item = new LinkedHashMap<>();
item.put("column", a.column);
item.put("stats", a.stats);
item.put("trend", a.trend);
item.put("seasonality", a.seasonality);
item.put("anomaly_count", a.anomalies.size());
List<Map<String, Object>> anOut = new ArrayList<>();
for (Map<String, Object> an : a.anomalies) {
Map<String, Object> mapped = new LinkedHashMap<>();
mapped.put("timestamp", an.get("timestamp"));
mapped.put("value", an.get("value"));
mapped.put("moving_average", an.get("moving_average"));
mapped.put("z_from_moving_average", an.get("z_from_moving_average"));
anOut.add(mapped);
}
item.put("anomalies", anOut);
analysisOut.add(item);
}
Map<String, Object> report = new LinkedHashMap<>();
report.put("metadata", metadata);
report.put("timestamp_column", timestampColumn);
report.put("value_columns", valueColumns);
report.put("analyses", analysisOut);
return report;
}
private static void exportProcessedCsv(Path path, List<Row> rows, List<Analysis> analyses) throws IOException {
try (BufferedWriter writer = Files.newBufferedWriter(path, StandardCharsets.UTF_8)) {
List<String> header = new ArrayList<>();
header.add("timestamp");
for (Analysis a : analyses) {
header.add(a.column);
header.add(a.column + "_moving_average");
header.add(a.column + "_is_anomaly");
}
writer.write(String.join(",", header));
writer.write("\n");
for (int i = 0; i < rows.size(); i++) {
List<String> line = new ArrayList<>();
line.add(csvEscape(rows.get(i).isoTimestamp));
for (Analysis a : analyses) {
Double value = rows.get(i).values.get(a.column);
Double ma = a.movingAverage.get(i);
boolean isAnomaly = a.anomalies.stream().anyMatch(x -> ((Number) x.get("index")).intValue() == i);
line.add(csvEscape(value));
line.add(csvEscape(ma));
line.add(isAnomaly ? "1" : "0");
}
writer.write(String.join(",", line));
writer.write("\n");
}
}
}
private static String csvEscape(Object value) {
if (value == null) return "";
String s = String.valueOf(value);
if (s.contains(",") || s.contains("\"") || s.contains("\n")) return "\"" + s.replace("\"", "\"\"") + "\"";
return s;
}
private static void printSummary(List<Analysis> analyses, int gapCount, Path outputPath, Path exportPath) {
System.out.println("Time Series Trend Detector");
System.out.println("==========================");
System.out.println("Columns analyzed: " + analyses.size());
System.out.println("Gap points filled/interpolated: " + gapCount);
System.out.println();
for (Analysis a : analyses) {
System.out.println("Column: " + a.column);
System.out.printf(" Trend : %s (slope=%.6f, R^2=%.4f)%n",
a.trend.get("direction"), (double) a.trend.get("slope"), (double) a.trend.get("r_squared"));
if (a.seasonality.get("period") == null) {
System.out.println(" Seasonality: none");
} else {
System.out.printf(" Seasonality: period=%s (autocorr=%.4f)%n",
a.seasonality.get("period"), (double) a.seasonality.get("autocorrelation"));
}
System.out.println(" Anomalies : " + a.anomalies.size());
System.out.printf(" Stats : min=%s, max=%s, mean=%s, variance=%s%n",
a.stats.get("min"), a.stats.get("max"), a.stats.get("mean"), a.stats.get("variance"));
System.out.println();
}
System.out.println("JSON report saved : " + outputPath);
if (exportPath != null) System.out.println("Processed CSV saved: " + exportPath);
}
private static String toJson(Object obj, int indent) {
String pad = " ".repeat(indent);
if (obj == null) return "null";
if (obj instanceof String s) return "\"" + s.replace("\\", "\\\\").replace("\"", "\\\"") + "\"";
if (obj instanceof Number || obj instanceof Boolean) return String.valueOf(obj);
if (obj instanceof Map<?, ?> map) {
StringBuilder out = new StringBuilder();
out.append("{\n");
int i = 0;
for (Map.Entry<?, ?> e : map.entrySet()) {
out.append(pad).append(" ").append(toJson(String.valueOf(e.getKey()), 0)).append(": ")
.append(toJson(e.getValue(), indent + 1));
if (++i < map.size()) out.append(",");
out.append("\n");
}
out.append(pad).append("}");
return out.toString();
}
if (obj instanceof List<?> list) {
StringBuilder out = new StringBuilder();
out.append("[\n");
for (int i = 0; i < list.size(); i++) {
out.append(pad).append(" ").append(toJson(list.get(i), indent + 1));
if (i + 1 < list.size()) out.append(",");
out.append("\n");
}
out.append(pad).append("]");
return out.toString();
}
return toJson(String.valueOf(obj), indent);
}
}