|
| 1 | +#include "latest_finder.h" |
| 2 | +#include "common_arg_parser.h" |
| 3 | +#include "data_reader.h" |
| 4 | +#include <iostream> |
| 5 | +#include <iomanip> |
| 6 | +#include <map> |
| 7 | +#include <vector> |
| 8 | +#include <algorithm> |
| 9 | +#include <ctime> |
| 10 | +#include <cstring> |
| 11 | + |
| 12 | +// Structure to hold latest reading info per sensor |
| 13 | +struct SensorLatest { |
| 14 | + std::string sensorId; |
| 15 | + long long timestamp; |
| 16 | + |
| 17 | + SensorLatest() : timestamp(0) {} |
| 18 | +}; |
| 19 | + |
| 20 | +LatestFinder::LatestFinder(int argc, char* argv[]) : limitRows(0), outputFormat("human") { |
| 21 | + // Check for help flag first |
| 22 | + for (int i = 1; i < argc; ++i) { |
| 23 | + std::string arg = argv[i]; |
| 24 | + if (arg == "--help" || arg == "-h") { |
| 25 | + usage(); |
| 26 | + exit(0); |
| 27 | + } |
| 28 | + } |
| 29 | + |
| 30 | + // Parse custom arguments first and build filtered argv |
| 31 | + std::vector<char*> filteredArgv; |
| 32 | + for (int i = 0; i < argc; i++) { |
| 33 | + std::string arg = argv[i]; |
| 34 | + if (arg == "-n" && i + 1 < argc) { |
| 35 | + limitRows = std::atoi(argv[i + 1]); |
| 36 | + i++; // Skip the value |
| 37 | + continue; |
| 38 | + } |
| 39 | + if ((arg == "-of" || arg == "--output-format") && i + 1 < argc) { |
| 40 | + outputFormat = argv[i + 1]; |
| 41 | + i++; // Skip the value |
| 42 | + continue; |
| 43 | + } |
| 44 | + filteredArgv.push_back(argv[i]); |
| 45 | + } |
| 46 | + |
| 47 | + CommonArgParser parser; |
| 48 | + if (!parser.parse(static_cast<int>(filteredArgv.size()), filteredArgv.data())) { |
| 49 | + exit(1); |
| 50 | + } |
| 51 | + |
| 52 | + // Check for unknown options |
| 53 | + std::string unknownOpt = CommonArgParser::checkUnknownOptions( |
| 54 | + static_cast<int>(filteredArgv.size()), filteredArgv.data()); |
| 55 | + if (!unknownOpt.empty()) { |
| 56 | + std::cerr << "Error: Unknown option '" << unknownOpt << "'" << std::endl; |
| 57 | + usage(); |
| 58 | + exit(1); |
| 59 | + } |
| 60 | + |
| 61 | + copyFromParser(parser); |
| 62 | +} |
| 63 | + |
| 64 | +void LatestFinder::usage() { |
| 65 | + std::cerr << "Usage: sensor-data latest [OPTIONS] <file(s)/directory>\n" |
| 66 | + << " Outputs the latest timestamp for each sensor_id\n" |
| 67 | + << "\nOptions:\n" |
| 68 | + << " -n <num> Limit output rows (positive = first n, negative = last n)\n" |
| 69 | + << " -of, --output-format <fmt> Output format: human (default), csv, or json\n" |
| 70 | + << " --min-date <date> Only consider readings after this date\n" |
| 71 | + << " --max-date <date> Only consider readings before this date\n" |
| 72 | + << " -if, --input-format <fmt> Input format: json (default) or csv\n" |
| 73 | + << " --tail <n> Only read last n lines from each file\n" |
| 74 | + << " -v, --verbose Show verbose output\n" |
| 75 | + << " -h, --help Show this help message\n" |
| 76 | + << "\nOutput columns: sensor_id, unix_timestamp, iso_date\n"; |
| 77 | +} |
| 78 | + |
| 79 | +int LatestFinder::main() { |
| 80 | + if (inputFiles.empty()) { |
| 81 | + std::cerr << "Error: No input files specified\n"; |
| 82 | + usage(); |
| 83 | + return 1; |
| 84 | + } |
| 85 | + |
| 86 | + printCommonVerboseInfo("latest", verbosity, recursive, extensionFilter, maxDepth, inputFiles.size()); |
| 87 | + |
| 88 | + // Map to store latest timestamp per sensor_id |
| 89 | + std::map<std::string, SensorLatest> latestBySensor; |
| 90 | + |
| 91 | + // Create data reader with date filters |
| 92 | + DataReader reader(minDate, maxDate, verbosity, inputFormat, tailLines); |
| 93 | + |
| 94 | + // Process all files |
| 95 | + for (const std::string& file : inputFiles) { |
| 96 | + if (verbosity > 0) { |
| 97 | + std::cerr << "Processing: " << file << "\n"; |
| 98 | + } |
| 99 | + |
| 100 | + reader.processFile(file, [&](const std::map<std::string, std::string>& reading, int, const std::string&) { |
| 101 | + // Get sensor_id |
| 102 | + auto sensorIt = reading.find("sensor_id"); |
| 103 | + if (sensorIt == reading.end() || sensorIt->second.empty()) { |
| 104 | + return; |
| 105 | + } |
| 106 | + std::string sensorId = sensorIt->second; |
| 107 | + |
| 108 | + // Get timestamp |
| 109 | + long long ts = DateUtils::getTimestamp(reading); |
| 110 | + if (ts <= 0) return; |
| 111 | + |
| 112 | + // Update if this is the latest for this sensor |
| 113 | + auto& entry = latestBySensor[sensorId]; |
| 114 | + if (ts > entry.timestamp) { |
| 115 | + entry.sensorId = sensorId; |
| 116 | + entry.timestamp = ts; |
| 117 | + } |
| 118 | + }); |
| 119 | + } |
| 120 | + |
| 121 | + // Convert to vector for sorting and limiting |
| 122 | + std::vector<SensorLatest> results; |
| 123 | + for (const auto& pair : latestBySensor) { |
| 124 | + results.push_back(pair.second); |
| 125 | + } |
| 126 | + |
| 127 | + // Sort by sensor_id (natural order from map) |
| 128 | + std::sort(results.begin(), results.end(), [](const SensorLatest& a, const SensorLatest& b) { |
| 129 | + return a.sensorId < b.sensorId; |
| 130 | + }); |
| 131 | + |
| 132 | + // Apply -n limiting |
| 133 | + size_t startIdx = 0; |
| 134 | + size_t endIdx = results.size(); |
| 135 | + |
| 136 | + if (limitRows != 0) { |
| 137 | + if (limitRows > 0) { |
| 138 | + // First n rows |
| 139 | + endIdx = std::min(static_cast<size_t>(limitRows), results.size()); |
| 140 | + } else { |
| 141 | + // Last n rows (negative) |
| 142 | + size_t count = static_cast<size_t>(-limitRows); |
| 143 | + if (count < results.size()) { |
| 144 | + startIdx = results.size() - count; |
| 145 | + } |
| 146 | + } |
| 147 | + } |
| 148 | + |
| 149 | + // Output results |
| 150 | + if (outputFormat == "json") { |
| 151 | + std::cout << "["; |
| 152 | + bool first = true; |
| 153 | + for (size_t i = startIdx; i < endIdx; ++i) { |
| 154 | + const SensorLatest& entry = results[i]; |
| 155 | + char isoDate[32]; |
| 156 | + time_t t = static_cast<time_t>(entry.timestamp); |
| 157 | + std::strftime(isoDate, sizeof(isoDate), "%Y-%m-%d %H:%M:%S", std::localtime(&t)); |
| 158 | + |
| 159 | + if (!first) std::cout << ","; |
| 160 | + first = false; |
| 161 | + std::cout << "{\"sensor_id\":\"" << entry.sensorId |
| 162 | + << "\",\"timestamp\":" << entry.timestamp |
| 163 | + << ",\"iso_date\":\"" << isoDate << "\"}"; |
| 164 | + } |
| 165 | + std::cout << "]\n"; |
| 166 | + } else if (outputFormat == "csv") { |
| 167 | + std::cout << "sensor_id,timestamp,iso_date\n"; |
| 168 | + for (size_t i = startIdx; i < endIdx; ++i) { |
| 169 | + const SensorLatest& entry = results[i]; |
| 170 | + char isoDate[32]; |
| 171 | + time_t t = static_cast<time_t>(entry.timestamp); |
| 172 | + std::strftime(isoDate, sizeof(isoDate), "%Y-%m-%d %H:%M:%S", std::localtime(&t)); |
| 173 | + std::cout << entry.sensorId << "," << entry.timestamp << "," << isoDate << "\n"; |
| 174 | + } |
| 175 | + } else { |
| 176 | + // Human-readable format (default) |
| 177 | + // Find max sensor_id width for alignment |
| 178 | + size_t maxIdWidth = 9; // "sensor_id" |
| 179 | + for (size_t i = startIdx; i < endIdx; ++i) { |
| 180 | + maxIdWidth = std::max(maxIdWidth, results[i].sensorId.length()); |
| 181 | + } |
| 182 | + |
| 183 | + std::cout << "Latest readings by sensor:\n\n"; |
| 184 | + std::cout << std::left; |
| 185 | + std::cout.width(maxIdWidth + 2); |
| 186 | + std::cout << "Sensor ID"; |
| 187 | + std::cout.width(14); |
| 188 | + std::cout << "Timestamp"; |
| 189 | + std::cout << "Date/Time\n"; |
| 190 | + std::cout << std::string(maxIdWidth + 2 + 14 + 19, '-') << "\n"; |
| 191 | + |
| 192 | + for (size_t i = startIdx; i < endIdx; ++i) { |
| 193 | + const SensorLatest& entry = results[i]; |
| 194 | + char isoDate[32]; |
| 195 | + time_t t = static_cast<time_t>(entry.timestamp); |
| 196 | + std::strftime(isoDate, sizeof(isoDate), "%Y-%m-%d %H:%M:%S", std::localtime(&t)); |
| 197 | + |
| 198 | + std::cout.width(maxIdWidth + 2); |
| 199 | + std::cout << entry.sensorId; |
| 200 | + std::cout.width(14); |
| 201 | + std::cout << entry.timestamp; |
| 202 | + std::cout << isoDate << "\n"; |
| 203 | + } |
| 204 | + |
| 205 | + std::cout << "\nTotal: " << (endIdx - startIdx) << " sensor(s)\n"; |
| 206 | + } |
| 207 | + |
| 208 | + return 0; |
| 209 | +} |
0 commit comments