From bfd05bd2499bf8abf20265e0972e4f2d4536d9af Mon Sep 17 00:00:00 2001 From: Nedifinita Date: Sat, 27 Sep 2025 00:56:09 +0800 Subject: [PATCH] feat: implement time window algorithm --- lib/services/merge_service.dart | 192 ++++++++++++++++++++++++++++---- pubspec.yaml | 2 +- 2 files changed, 174 insertions(+), 20 deletions(-) diff --git a/lib/services/merge_service.dart b/lib/services/merge_service.dart index a1ba062..a2264e6 100644 --- a/lib/services/merge_service.dart +++ b/lib/services/merge_service.dart @@ -36,24 +36,15 @@ class MergeService { return allRecords; } - final now = DateTime.now(); - final validRecords = settings.timeWindow.duration == null - ? allRecords - : allRecords - .where((r) => - now.difference(r.receivedTimestamp) <= - settings.timeWindow.duration!) - .toList(); - - validRecords + allRecords .sort((a, b) => b.receivedTimestamp.compareTo(a.receivedTimestamp)); if (settings.groupBy == GroupBy.trainOrLoco) { - return _groupByTrainOrLoco(validRecords); + return _groupByTrainOrLocoWithTimeWindow(allRecords, settings.timeWindow); } final groupedRecords = >{}; - for (final record in validRecords) { + for (final record in allRecords) { final key = _generateGroupKey(record, settings.groupBy); if (key != null) { groupedRecords.putIfAbsent(key, () => []).add(record); @@ -62,23 +53,34 @@ class MergeService { final List mergedRecords = []; final Set mergedRecordIds = {}; + final List discardedRecords = []; groupedRecords.forEach((key, group) { - if (group.length >= 2) { + final processedGroup = _applyTimeWindow(group, settings.timeWindow); + + if (processedGroup.length >= 2) { mergedRecords.add(MergedTrainRecord( groupKey: key, - records: group, - latestRecord: group.first, + records: processedGroup, + latestRecord: processedGroup.first, )); - for (final record in group) { + for (final record in processedGroup) { mergedRecordIds.add(record.uniqueId); } } + + for (final record in group) { + if (!processedGroup.contains(record)) { + discardedRecords.add(record); + } + } }); - final singleRecords = validRecords - .where((r) => !mergedRecordIds.contains(r.uniqueId)) - .toList(); + final reusedRecords = _reuseDiscardedRecords( + discardedRecords, mergedRecordIds, settings.groupBy); + + final singleRecords = + allRecords.where((r) => !mergedRecordIds.contains(r.uniqueId)).toList(); final List mixedList = [...mergedRecords, ...singleRecords]; mixedList.sort((a, b) { @@ -94,6 +96,158 @@ class MergeService { return mixedList; } + static List _applyTimeWindow( + List group, TimeWindow timeWindow) { + if (timeWindow.duration == null) { + return group; + } + + group.sort((a, b) => a.receivedTimestamp.compareTo(b.receivedTimestamp)); + + while (group.length > 1) { + final timeSpan = group.last.receivedTimestamp + .difference(group.first.receivedTimestamp); + + if (timeSpan <= timeWindow.duration!) { + break; + } + + group.removeAt(0); + } + + group.sort((a, b) => b.receivedTimestamp.compareTo(a.receivedTimestamp)); + return group; + } + + static List _reuseDiscardedRecords( + List discardedRecords, + Set mergedRecordIds, + GroupBy groupBy) { + final reusedRecords = []; + + for (final record in discardedRecords) { + if (mergedRecordIds.contains(record.uniqueId)) continue; + + final key = _generateGroupKey(record, groupBy); + if (key != null) { + reusedRecords.add(record); + } + } + + return reusedRecords; + } + + static List _groupByTrainOrLocoWithTimeWindow( + List records, TimeWindow timeWindow) { + final List mergedRecords = []; + final List singleRecords = []; + final Set usedRecordIds = {}; + + for (int i = 0; i < records.length; i++) { + final record = records[i]; + if (usedRecordIds.contains(record.uniqueId)) continue; + + final group = [record]; + + for (int j = i + 1; j < records.length; j++) { + final otherRecord = records[j]; + if (usedRecordIds.contains(otherRecord.uniqueId)) continue; + + final recordTrain = record.train.trim(); + final otherTrain = otherRecord.train.trim(); + final recordLoco = record.loco.trim(); + final otherLoco = otherRecord.loco.trim(); + + final trainMatch = recordTrain.isNotEmpty && + recordTrain != "" && + !recordTrain.contains("-----") && + otherTrain.isNotEmpty && + otherTrain != "" && + !otherTrain.contains("-----") && + recordTrain == otherTrain; + + final locoMatch = recordLoco.isNotEmpty && + recordLoco != "" && + otherLoco.isNotEmpty && + otherLoco != "" && + recordLoco == otherLoco; + + final bothTrainEmpty = (recordTrain.isEmpty || + recordTrain == "" || + recordTrain.contains("----")) && + (otherTrain.isEmpty || + otherTrain == "" || + otherTrain.contains("----")); + + if (trainMatch || locoMatch || (bothTrainEmpty && locoMatch)) { + group.add(otherRecord); + } + } + + final processedGroup = _applyTimeWindow(group, timeWindow); + + if (processedGroup.length >= 2) { + for (final record in processedGroup) { + usedRecordIds.add(record.uniqueId); + } + + final firstRecord = processedGroup.first; + final train = firstRecord.train.trim(); + final loco = firstRecord.loco.trim(); + String uniqueGroupKey; + + if (train.isNotEmpty && + train != "" && + !train.contains("-----") && + loco.isNotEmpty && + loco != "") { + uniqueGroupKey = "train_or_loco:${train}_$loco"; + } else if (train.isNotEmpty && + train != "" && + !train.contains("-----") && + loco.isEmpty) { + uniqueGroupKey = "train_or_loco:train:$train"; + } else if (loco.isNotEmpty && loco != "") { + uniqueGroupKey = "train_or_loco:loco:$loco"; + } else { + uniqueGroupKey = "train_or_loco:group_${mergedRecords.length}"; + } + + mergedRecords.add(MergedTrainRecord( + groupKey: uniqueGroupKey, + records: processedGroup, + latestRecord: processedGroup.first, + )); + } else { + // 处理被丢弃的记录 + for (final record in group) { + if (!processedGroup.contains(record)) { + singleRecords.add(record); + usedRecordIds.add(record.uniqueId); + } + } + + if (processedGroup.isNotEmpty) { + singleRecords.add(processedGroup.first); + usedRecordIds.add(processedGroup.first.uniqueId); + } + } + } + + final List result = [...mergedRecords, ...singleRecords]; + result.sort((a, b) { + final aTime = a is MergedTrainRecord + ? a.latestRecord.receivedTimestamp + : (a as TrainRecord).receivedTimestamp; + final bTime = b is MergedTrainRecord + ? b.latestRecord.receivedTimestamp + : (b as TrainRecord).receivedTimestamp; + return bTime.compareTo(aTime); + }); + + return result; + } + static List _groupByTrainOrLoco(List records) { final List mergedRecords = []; final List singleRecords = []; diff --git a/pubspec.yaml b/pubspec.yaml index 3d92643..1c2da19 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 0.3.0-flutter+30 +version: 0.4.0-flutter+40 environment: sdk: ^3.5.4