Time Series Trend Detector (cpp, written by Codex)
envgap__codex__cpp-t1-9
Written by a coding agent; not on GitHubWritten 2026-03-02
01 / FAILURE SIGNATURE
As the study recorded it
No identifying execution failure has been captured.
Not a benchmark task.
- The project already builds and runs before the fix, so there is nothing to repair.
02 / ENVIRONMENT RECIPE
- Base commit
Not freshly verified- Manifest
CMakeLists.txt- Reproduce
Awaiting issue-specific recipe- Run under trace
Awaiting a meaningful runtime command
03 / TASK AND FAILURE
codex/cpp-t1 #9 · read the task the agent was given
Codex wrote this cpp project from the task below. It installed and ran 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 C++ project for a clean Ubuntu 22.04 machine with only G++ 12+ and CMake 3.22+ installed. Include: - Source code - CMakeLists.txt 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.
CMakeLists.txt
cmake_minimum_required(VERSION 3.22) project(time_series_trend_detector_cpp VERSION 1.0.0 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) include(FetchContent) # Pinned dependency FetchContent_Declare( nlohmann_json URL https://github.com/nlohmann/json/releases/download/v3.11.3/json.tar.xz ) FetchContent_MakeAvailable(nlohmann_json) add_executable(trend_detector src/main.cpp) target_link_libraries(trend_detector PRIVATE nlohmann_json::nlohmann_json)
README.md
# Time Series Trend Detector (C++) Analyzes CSV time series data to detect: - moving-average trends - regression trend direction and strength - autocorrelation-based seasonality - anomaly points beyond a configurable threshold - missing timestamp gaps with interpolation ## Requirements - Ubuntu 22.04 - G++ 12+ - CMake 3.22+ - Network access during CMake configure (dependency fetch) ## Dependencies - Direct (pinned in `CMakeLists.txt`): - `nlohmann/json v3.11.3` - Transitive: - none ## Build ```bash cmake -S . -B build cmake --build build ``` ## Run With input: ```bash ./build/trend_detector /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 ./build/trend_detector ```
src/main.cpp
#include <algorithm>
#include <cmath>
#include <ctime>
#include <filesystem>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <optional>
#include <regex>
#include <sstream>
#include <string>
#include <vector>
#include <nlohmann/json.hpp>
using json = nlohmann::json;
namespace {
double kNaN = std::numeric_limits<double>::quiet_NaN();
struct ParsedArgs {
std::map<std::string, std::string> options;
std::vector<std::string> positional;
};
struct Row {
long long timestampMs = 0;
std::string isoTimestamp;
std::map<std::string, double> values;
bool gapFilled = false;
};
struct ParseResult {
std::vector<Row> rows;
std::vector<std::string> valueColumns;
std::string timestampColumn;
};
struct GapResult {
std::vector<Row> rows;
long long inferredStepMs = 86400000;
std::vector<long long> gapPoints;
};
struct Regression {
double slope = 0.0;
double intercept = 0.0;
double r2 = 0.0;
};
struct Seasonality {
std::optional<int> period;
std::optional<double> autocorrelation;
};
struct Analysis {
std::string column;
std::map<std::string, double> stats;
std::string trendDirection;
double trendSlope = 0.0;
double trendR2 = 0.0;
Seasonality seasonality;
std::vector<json> anomalies;
std::vector<double> movingAverage;
};
ParsedArgs parseArgs(int argc, char** argv) {
ParsedArgs out;
for (int i = 1; i < argc; i++) {
std::string token = argv[i];
if (token.rfind("--", 0) == 0) {
std::string key = token.substr(2);
if (i + 1 < argc && std::string(argv[i + 1]).rfind("--", 0) != 0) out.options[key] = argv[++i];
else out.options[key] = "true";
} else {
out.positional.push_back(token);
}
}
return out;
}
std::vector<std::string> parseCsvLine(const std::string& line) {
std::vector<std::string> out;
std::string current;
bool inQuotes = false;
for (std::size_t i = 0; i < line.size(); i++) {
char ch = line[i];
if (ch == '"') {
if (inQuotes && i + 1 < line.size() && line[i + 1] == '"') {
current += '"';
i++;
} else {
inQuotes = !inQuotes;
}
} else if (ch == ',' && !inQuotes) {
out.push_back(current);
current.clear();
} else {
current += ch;
}
}
out.push_back(current);
return out;
}
std::time_t toUtcTimestamp(std::tm tm) {
#ifdef _WIN32
return _mkgmtime(&tm);
#else
return timegm(&tm);
#endif
}
std::optional<long long> parseTimestampMs(const std::string& raw) {
std::string s = std::regex_replace(raw, std::regex(R"(^\s+|\s+$)"), "");
if (s.empty()) return std::nullopt;
if (std::regex_match(s, std::regex(R"(^\d{10}$)"))) return std::stoll(s) * 1000LL;
if (std::regex_match(s, std::regex(R"(^\d{13}$)"))) return std::stoll(s);
if (std::regex_match(s, std::regex(R"(^\d{1,2}/\d{1,2}/\d{4}$)"))) {
std::tm tm{};
std::istringstream in(s);
in >> std::get_time(&tm, "%m/%d/%Y");
if (!in.fail()) return static_cast<long long>(toUtcTimestamp(tm)) * 1000LL;
}
if (std::regex_match(s, std::regex(R"(^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$)"))) {
std::tm tm{};
std::istringstream in(s);
in >> std::get_time(&tm, "%Y-%m-%d %H:%M:%S");
if (!in.fail()) return static_cast<long long>(toUtcTimestamp(tm)) * 1000LL;
}
if (s.size() >= 10 && s[4] == '-' && s[7] == '-') {
std::string norm = s;
if (norm.size() == 10) norm += "T00:00:00";
if (norm.size() >= 19 && norm[10] == ' ') norm[10] = 'T';
std::tm tm{};
std::istringstream in(norm.substr(0, 19));
in >> std::get_time(&tm, "%Y-%m-%dT%H:%M:%S");
if (!in.fail()) return static_cast<long long>(toUtcTimestamp(tm)) * 1000LL;
}
return std::nullopt;
}
std::string isoFromMs(long long ms) {
std::time_t t = static_cast<std::time_t>(ms / 1000LL);
std::tm tm{};
#ifdef _WIN32
gmtime_s(&tm, &t);
#else
gmtime_r(&t, &tm);
#endif
char buf[32];
std::strftime(buf, sizeof(buf), "%Y-%m-%dT%H:%M:%SZ", &tm);
return buf;
}
ParseResult parseInputCsv(const std::filesystem::path& path) {
std::ifstream in(path);
if (!in.is_open()) throw std::runtime_error("Failed to open input CSV: " + path.string());
std::string line;
if (!std::getline(in, line)) return {};
if (!line.empty() && static_cast<unsigned char>(line[0]) == 0xEF && line.size() >= 3 &&
static_cast<unsigned char>(line[1]) == 0xBB && static_cast<unsigned char>(line[2]) == 0xBF) {
line = line.substr(3);
}
auto headers = parseCsvLine(line);
if (headers.size() < 2) return {};
std::string tsCol = headers[0];
std::vector<std::string> valueCols(headers.begin() + 1, headers.end());
std::vector<Row> rows;
while (std::getline(in, line)) {
if (!line.empty() && line.back() == '\r') line.pop_back();
if (line.empty()) continue;
auto parts = parseCsvLine(line);
auto ts = parseTimestampMs(parts[0]);
if (!ts.has_value()) continue;
Row row;
row.timestampMs = *ts;
row.isoTimestamp = isoFromMs(*ts);
row.gapFilled = false;
for (std::size_t i = 0; i < valueCols.size(); i++) {
double v = kNaN;
if (i + 1 < parts.size()) {
std::string raw = std::regex_replace(parts[i + 1], std::regex(R"(^\s+|\s+$)"), "");
if (!raw.empty()) {
try { v = std::stod(raw); } catch (...) {}
}
}
row.values[valueCols[i]] = v;
}
rows.push_back(row);
}
return {rows, valueCols, tsCol};
}
void generateSample(const std::filesystem::path& path) {
std::ofstream out(path);
out << "timestamp,metric_a,metric_b\n";
long long start = 1735689600000LL; // 2025-01-01T00:00:00Z
for (int i = 0; i < 365; i++) {
long long ts = start + static_cast<long long>(i) * 86400000LL;
double weekly = 10.0 * std::sin((2.0 * M_PI * i) / 7.0);
double trend = i * 0.18;
double a = 50 + trend + weekly + std::sin(i) * 0.8;
double b = 30 + i * 0.05 + 5 * std::cos((2.0 * M_PI * i) / 7.0);
if (i == 45 || i == 123 || i == 251) a += 35;
if (i == 200 || i == 300) b -= 20;
out << isoFromMs(ts).substr(0, 10) << "," << std::fixed << std::setprecision(3) << a << "," << b << "\n";
}
}
bool isNan(double v) { return std::isnan(v); }
double mean(const std::vector<double>& values) {
if (values.empty()) return kNaN;
return std::accumulate(values.begin(), values.end(), 0.0) / values.size();
}
double variance(const std::vector<double>& values, double m) {
if (values.empty()) return kNaN;
double acc = 0.0;
for (double v : values) acc += (v - m) * (v - m);
return acc / values.size();
}
double stddev(const std::vector<double>& values, double m) {
double var = variance(values, m);
return isNan(var) ? kNaN : std::sqrt(var);
}
std::vector<double> movingAverage(const std::vector<double>& values, int window) {
std::vector<double> out(values.size(), kNaN);
for (std::size_t i = 0; i < values.size(); i++) {
std::size_t start = (i + 1 < static_cast<std::size_t>(window)) ? 0 : i + 1 - window;
std::vector<double> chunk;
for (std::size_t j = start; j <= i; j++) if (!isNan(values[j])) chunk.push_back(values[j]);
out[i] = chunk.empty() ? kNaN : mean(chunk);
}
return out;
}
Regression linearRegression(const std::vector<double>& values) {
std::vector<double> xs, ys;
for (std::size_t i = 0; i < values.size(); i++) {
if (!isNan(values[i])) {
xs.push_back(static_cast<double>(i));
ys.push_back(values[i]);
}
}
if (ys.size() < 2) return {};
double mx = mean(xs);
double my = mean(ys);
double cov = 0.0;
double vx = 0.0;
for (std::size_t i = 0; i < ys.size(); i++) {
cov += (xs[i] - mx) * (ys[i] - my);
vx += (xs[i] - mx) * (xs[i] - mx);
}
double slope = vx == 0.0 ? 0.0 : cov / vx;
double intercept = my - slope * mx;
double ssRes = 0.0;
double ssTot = 0.0;
for (std::size_t i = 0; i < ys.size(); i++) {
double pred = slope * xs[i] + intercept;
ssRes += (ys[i] - pred) * (ys[i] - pred);
ssTot += (ys[i] - my) * (ys[i] - my);
}
double r2 = ssTot == 0.0 ? 0.0 : 1.0 - ssRes / ssTot;
return {slope, intercept, r2};
}
std::optional<double> autocorrelation(const std::vector<double>& values, int lag) {
std::vector<double> xs, ys;
for (std::size_t i = lag; i < values.size(); i++) {
if (isNan(values[i]) || isNan(values[i - lag])) continue;
xs.push_back(values[i]);
ys.push_back(values[i - lag]);
}
if (xs.size() < 3) return std::nullopt;
double mx = mean(xs), my = mean(ys);
double num = 0.0, dx = 0.0, dy = 0.0;
for (std::size_t i = 0; i < xs.size(); i++) {
num += (xs[i] - mx) * (ys[i] - my);
dx += (xs[i] - mx) * (xs[i] - mx);
dy += (ys[i] - my) * (ys[i] - my);
}
double den = std::sqrt(dx * dy);
return den == 0.0 ? 0.0 : num / den;
}
Seasonality detectSeasonality(const std::vector<double>& values) {
int maxLag = std::min(60, static_cast<int>(values.size() / 2));
int bestLag = -1;
double bestCorr = 0.0;
for (int lag = 2; lag <= maxLag; lag++) {
auto corr = autocorrelation(values, lag);
if (!corr.has_value()) continue;
if (std::abs(*corr) > std::abs(bestCorr)) {
bestCorr = *corr;
bestLag = lag;
}
}
if (bestLag < 0 || std::abs(bestCorr) < 0.3) return {};
return {bestLag, bestCorr};
}
long long inferStepMs(const std::vector<Row>& rows) {
std::vector<long long> diffs;
for (std::size_t i = 1; i < rows.size(); i++) {
long long d = rows[i].timestampMs - rows[i - 1].timestampMs;
if (d > 0) diffs.push_back(d);
}
if (diffs.empty()) return 86400000LL;
std::sort(diffs.begin(), diffs.end());
return diffs[diffs.size() / 2];
}
GapResult interpolateGaps(std::vector<Row> rows, const std::vector<std::string>& valueCols) {
std::sort(rows.begin(), rows.end(), [](const Row& a, const Row& b) { return a.timestampMs < b.timestampMs; });
long long step = inferStepMs(rows);
std::map<long long, Row> byTs;
for (const auto& r : rows) byTs[r.timestampMs] = r;
long long start = rows.front().timestampMs;
long long end = rows.back().timestampMs;
std::vector<Row> filled;
std::vector<long long> gaps;
for (long long t = start; t <= end; t += step) {
if (byTs.count(t)) {
Row r = byTs[t];
r.gapFilled = false;
filled.push_back(r);
} else {
Row r;
r.timestampMs = t;
r.isoTimestamp = isoFromMs(t);
r.gapFilled = true;
for (const auto& c : valueCols) r.values[c] = kNaN;
filled.push_back(r);
gaps.push_back(t);
}
}
for (const auto& col : valueCols) {
for (std::size_t i = 0; i < filled.size(); i++) {
if (!isNan(filled[i].values[col])) continue;
int left = static_cast<int>(i) - 1;
while (left >= 0 && isNan(filled[left].values[col])) left--;
int right = static_cast<int>(i) + 1;
while (right < static_cast<int>(filled.size()) && isNan(filled[right].values[col])) right++;
if (left >= 0 && right < static_cast<int>(filled.size())) {
double lv = filled[left].values[col];
double rv = filled[right].values[col];
double ratio = static_cast<double>(filled[i].timestampMs - filled[left].timestampMs) /
static_cast<double>(filled[right].timestampMs - filled[left].timestampMs);
filled[i].values[col] = lv + (rv - lv) * ratio;
}
}
}
return {filled, step, gaps};
}
Analysis analyzeColumn(const std::vector<Row>& rows, const std::string& col, int window, double threshold) {
std::vector<double> values;
values.reserve(rows.size());
for (const auto& r : rows) values.push_back(r.values.at(col));
auto ma = movingAverage(values, window);
std::vector<double> valid;
for (double v : values) if (!isNan(v)) valid.push_back(v);
double mu = valid.empty() ? kNaN : mean(valid);
double var = valid.empty() ? kNaN : variance(valid, mu);
double sd = valid.empty() ? kNaN : stddev(valid, mu);
Regression reg = linearRegression(values);
Seasonality season = detectSeasonality(values);
std::string direction = "stable";
double scale = (isNan(sd) || sd == 0.0) ? 1.0 : sd;
if (std::abs(reg.slope) >= scale * 0.001) direction = reg.slope > 0 ? "increasing" : "decreasing";
std::vector<json> anomalies;
for (std::size_t i = 0; i < values.size(); i++) {
if (isNan(values[i]) || isNan(ma[i]) || isNan(sd) || sd == 0.0) continue;
double z = std::abs(values[i] - ma[i]) / sd;
if (z > threshold) {
anomalies.push_back({
{"index", i},
{"timestamp", rows[i].isoTimestamp},
{"value", values[i]},
{"moving_average", ma[i]},
{"z_from_moving_average", z}
});
}
}
std::map<std::string, double> stats;
stats["min"] = valid.empty() ? kNaN : *std::min_element(valid.begin(), valid.end());
stats["max"] = valid.empty() ? kNaN : *std::max_element(valid.begin(), valid.end());
stats["mean"] = mu;
stats["variance"] = var;
stats["stddev"] = sd;
return {col, stats, direction, reg.slope, reg.r2, season, anomalies, ma};
}
std::string csvEscape(const std::string& s) {
if (s.find(',') != std::string::npos || s.find('"') != std::string::npos || s.find('\n') != std::string::npos) {
std::string out = "\"";
for (char c : s) {
if (c == '"') out += "\"\"";
else out += c;
}
out += "\"";
return out;
}
return s;
}
std::string csvEscapeDouble(double v) {
if (isNan(v)) return "";
std::ostringstream out;
out << std::setprecision(12) << v;
return csvEscape(out.str());
}
void exportProcessedCsv(const std::filesystem::path& path, const std::vector<Row>& rows, const std::vector<Analysis>& analyses) {
std::ofstream out(path);
out << "timestamp";
for (const auto& a : analyses) {
out << "," << a.column << "," << a.column << "_moving_average," << a.column << "_is_anomaly";
}
out << "\n";
for (std::size_t i = 0; i < rows.size(); i++) {
out << csvEscape(rows[i].isoTimestamp);
for (const auto& a : analyses) {
bool anomaly = std::any_of(a.anomalies.begin(), a.anomalies.end(), [i](const json& j) {
return j["index"].get<std::size_t>() == i;
});
out << "," << csvEscapeDouble(rows[i].values.at(a.column))
<< "," << csvEscapeDouble(a.movingAverage[i])
<< "," << (anomaly ? "1" : "0");
}
out << "\n";
}
}
json toJsonNumber(double v) {
return isNan(v) ? json(nullptr) : json(v);
}
void printSummary(const std::vector<Analysis>& analyses, std::size_t gapCount,
const std::filesystem::path& outputPath, const std::optional<std::filesystem::path>& exportPath) {
std::cout << "Time Series Trend Detector\n";
std::cout << "==========================\n";
std::cout << "Columns analyzed: " << analyses.size() << "\n";
std::cout << "Gap points filled/interpolated: " << gapCount << "\n\n";
for (const auto& a : analyses) {
std::cout << "Column: " << a.column << "\n";
std::cout << " Trend : " << a.trendDirection << " (slope=" << std::fixed << std::setprecision(6)
<< a.trendSlope << ", R^2=" << std::setprecision(4) << a.trendR2 << ")\n";
if (!a.seasonality.period.has_value()) {
std::cout << " Seasonality: none\n";
} else {
std::cout << " Seasonality: period=" << *a.seasonality.period
<< " (autocorr=" << std::setprecision(4) << *a.seasonality.autocorrelation << ")\n";
}
std::cout << " Anomalies : " << a.anomalies.size() << "\n";
std::cout << " Stats : min=" << (isNan(a.stats.at("min")) ? std::string("null") : std::to_string(a.stats.at("min")))
<< ", max=" << (isNan(a.stats.at("max")) ? std::string("null") : std::to_string(a.stats.at("max")))
<< ", mean=" << (isNan(a.stats.at("mean")) ? std::string("null") : std::to_string(a.stats.at("mean")))
<< ", variance=" << (isNan(a.stats.at("variance")) ? std::string("null") : std::to_string(a.stats.at("variance")))
<< "\n\n";
}
std::cout << "JSON report saved : " << outputPath.string() << "\n";
if (exportPath.has_value()) std::cout << "Processed CSV saved: " << exportPath->string() << "\n";
}
} // namespace
int main(int argc, char** argv) {
ParsedArgs args = parseArgs(argc, argv);
int window = 7;
double threshold = 2.0;
if (args.options.count("window")) window = std::max(1, std::stoi(args.options["window"]));
if (args.options.count("threshold")) threshold = std::stod(args.options["threshold"]);
std::filesystem::path outputPath = std::filesystem::absolute(args.options.count("output") ? args.options["output"] : "trend_report.json");
std::optional<std::filesystem::path> exportPath;
if (args.options.count("export")) exportPath = std::filesystem::absolute(args.options["export"]);
try {
std::filesystem::path inputPath;
if (args.positional.empty()) {
inputPath = std::filesystem::absolute("sample_timeseries.csv");
generateSample(inputPath);
std::cout << "No input provided. Generated sample dataset: " << inputPath.string() << "\n";
} else {
inputPath = std::filesystem::absolute(args.positional[0]);
if (!std::filesystem::exists(inputPath)) {
std::cerr << "Input file not found: " << inputPath.string() << "\n";
return 1;
}
}
ParseResult parsed = parseInputCsv(inputPath);
if (parsed.rows.empty() || parsed.valueColumns.empty()) {
std::cerr << "No valid rows or value columns found.\n";
return 1;
}
GapResult gap = interpolateGaps(parsed.rows, parsed.valueColumns);
std::vector<Analysis> analyses;
for (const auto& c : parsed.valueColumns) analyses.push_back(analyzeColumn(gap.rows, c, window, threshold));
json report;
report["metadata"] = {
{"input_file", inputPath.string()},
{"generated_at", isoFromMs(static_cast<long long>(std::time(nullptr)) * 1000LL)},
{"window", window},
{"threshold", threshold},
{"inferred_step_millis", gap.inferredStepMs},
{"interpolated_gap_points", json::array()}
};
for (auto t : gap.gapPoints) report["metadata"]["interpolated_gap_points"].push_back(isoFromMs(t));
report["timestamp_column"] = parsed.timestampColumn;
report["value_columns"] = parsed.valueColumns;
report["analyses"] = json::array();
for (const auto& a : analyses) {
json item;
item["column"] = a.column;
item["stats"] = {
{"min", toJsonNumber(a.stats.at("min"))},
{"max", toJsonNumber(a.stats.at("max"))},
{"mean", toJsonNumber(a.stats.at("mean"))},
{"variance", toJsonNumber(a.stats.at("variance"))},
{"stddev", toJsonNumber(a.stats.at("stddev"))}
};
item["trend"] = {
{"direction", a.trendDirection},
{"slope", a.trendSlope},
{"r_squared", a.trendR2}
};
item["seasonality"] = {
{"period", a.seasonality.period.has_value() ? json(*a.seasonality.period) : json(nullptr)},
{"autocorrelation", a.seasonality.autocorrelation.has_value() ? json(*a.seasonality.autocorrelation) : json(nullptr)}
};
item["anomaly_count"] = a.anomalies.size();
item["anomalies"] = json::array();
for (const auto& an : a.anomalies) {
item["anomalies"].push_back({
{"timestamp", an["timestamp"]},
{"value", an["value"]},
{"moving_average", an["moving_average"]},
{"z_from_moving_average", an["z_from_moving_average"]}
});
}
report["analyses"].push_back(item);
}
std::ofstream out(outputPath);
out << report.dump(2) << "\n";
out.close();
if (exportPath.has_value()) exportProcessedCsv(*exportPath, gap.rows, analyses);
printSummary(analyses, gap.gapPoints.size(), outputPath, exportPath);
return 0;
} catch (const std::exception& e) {
std::cerr << "Failed: " << e.what() << "\n";
return 1;
}
}