diff --git a/gofaster/.vscode/settings.json b/gofaster/.vscode/settings.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/gofaster/.vscode/settings.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/win_text_editor/lib/modules/pdf_parse/services/excel_data_adjustment_service.dart b/win_text_editor/lib/modules/pdf_parse/services/excel_data_adjustment_service.dart new file mode 100644 index 0000000..efda16a --- /dev/null +++ b/win_text_editor/lib/modules/pdf_parse/services/excel_data_adjustment_service.dart @@ -0,0 +1,125 @@ +import 'package:excel/excel.dart'; + +class ExcelDataAdjustmentService { + static const Map palaceDegreeAdjustments = { + '子': 300, + '丑': 270, + '寅': 240, + '卯': 210, + '辰': 180, + '巳': 150, + '午': 120, + '未': 90, + '申': 60, + '酉': 30, + '戌': 0, + '亥': 330, + }; + + static final _degreeFormatRegex = RegExp( + r"^(\d+)\s*([^\d\s°']+)\s*(\d+)\s*(?:['′]\s*(\d{1,2})\s*)?$", + caseSensitive: false, + ); + + // 处理数据行,添加宫位数据列 + List processRowData( + List cells, + Map previousPalaces, + Sheet sheet, + int rowIndex, + int startCol, // 新增参数:起始列位置 + ) { + List currentRowPalaces = List.filled(14, null); + int outputCol = startCol; // 使用传入的起始列 + + for (int col = 4; col <= 17 && col < cells.length; col++) { + final cellValue = cells[col]; + + // 解析宫位 + final palace = _extractPalace(cellValue); + String palaceToWrite = palace; + + // 如果宫位无效或为R/D,则使用上一行的宫位 + if (palaceToWrite.isEmpty || palaceToWrite == 'R' || palaceToWrite == 'D') { + palaceToWrite = previousPalaces[col] ?? ''; + } else { + // 更新上一行的宫位 + previousPalaces[col] = palaceToWrite; + } + + currentRowPalaces[col - 4] = palaceToWrite; + + // 写入宫列 + sheet.cell(CellIndex.indexByColumnRow( + columnIndex: outputCol, + rowIndex: rowIndex + )).value = TextCellValue(palaceToWrite); + outputCol++; + + // 写入原始单元格(带修正) + final displayValue = _getFormattedValueWithAdjustment(cellValue, palaceToWrite); + sheet.cell(CellIndex.indexByColumnRow( + columnIndex: outputCol, + rowIndex: rowIndex + )).value = TextCellValue(displayValue); + outputCol++; + } + + return currentRowPalaces; + } + + String _getFormattedValueWithAdjustment(String originalValue, String palace) { + // 1. 标准化输入 + String normalized = originalValue.replaceAll('′', "'").replaceAll(' ', '').replaceAll(' ', ''); + + // 2. 解析并替换地支为度符号 + String withDegreeSymbol = _replaceZodiacWithDegreeSymbol(normalized); + + // 3. 应用宫位修正 + if (palace.isNotEmpty && palaceDegreeAdjustments.containsKey(palace)) { + return _adjustDegreeByPalace(withDegreeSymbol, palace); + } + + return withDegreeSymbol; + } + + String _replaceZodiacWithDegreeSymbol(String value) { + final match = _degreeFormatRegex.firstMatch(value); + if (match == null) return value; + + final degreeStr = match.group(1); + final minuteStr = match.group(3); + final secondStr = match.group(4); + + if (degreeStr == null || minuteStr == null) return value; + + return '${degreeStr}°${minuteStr.padLeft(2, '0')}' + '${secondStr != null ? "'${secondStr.padLeft(2, '0')}\"" : ""}'; + } + + String _adjustDegreeByPalace(String valueWithDegree, String palace) { + final match = RegExp(r"^(\d+)°(\d+)(?:['](\d{1,2}))?").firstMatch(valueWithDegree); + if (match == null) return valueWithDegree; + + final degreeStr = match.group(1); + final minuteStr = match.group(2); + final secondStr = match.group(3); + + if (degreeStr == null || minuteStr == null) return valueWithDegree; + + final originalDegree = int.tryParse(degreeStr) ?? 0; + final adjustment = palaceDegreeAdjustments[palace] ?? 0; + var adjustedDegree = originalDegree + adjustment; + + adjustedDegree %= 360; + if (adjustedDegree < 0) adjustedDegree += 360; + + return '${adjustedDegree}°${minuteStr.padLeft(2, '0')}' + '${secondStr != null ? "'${secondStr.padLeft(2, '0')}\"" : ""}'; + } + + String _extractPalace(String cellValue) { + final match = RegExp(r"[^\d\s°\'RDL]").firstMatch(cellValue.trim()); + return match?.group(0) ?? ''; + } +} diff --git a/win_text_editor/lib/modules/pdf_parse/services/excel_deal_service.dart b/win_text_editor/lib/modules/pdf_parse/services/excel_deal_service.dart index e79d819..4fa00ba 100644 --- a/win_text_editor/lib/modules/pdf_parse/services/excel_deal_service.dart +++ b/win_text_editor/lib/modules/pdf_parse/services/excel_deal_service.dart @@ -1,11 +1,15 @@ import 'dart:io'; - import 'package:excel/excel.dart'; import 'package:file_picker/file_picker.dart'; import 'package:win_text_editor/app/providers/logger.dart'; +import 'excel_data_adjustment_service.dart'; +import 'relationship_calculator_service.dart'; class ExcelDealService { static const String _logTag = 'ExcelDealService'; + final ExcelDataAdjustmentService _adjustmentService = ExcelDataAdjustmentService(); + final RelationshipCalculatorService _relationshipService = RelationshipCalculatorService(); + static const Map _columnTitles = { 4: '日', 5: '月', @@ -23,291 +27,109 @@ class ExcelDealService { 17: '凯', }; - static const List> harmonyGroups = [ - {'寅', '午', '戌'}, // 寅午戌合 - {'亥', '卯', '未'}, // 亥卯未合 - {'申', '子', '辰'}, // 申子辰合 - {'巳', '酉', '丑'}, // 巳酉丑合 - ]; - - static const List> conflictGroups = [ - {'子', '午'}, // 子午冲 - {'丑', '未'}, // 丑未冲 - {'寅', '申'}, // 寅申冲 - {'卯', '酉'}, // 卯酉冲 - {'辰', '戌'}, // 辰戌冲 - {'巳', '亥'}, // 巳亥冲 - ]; - - static const List> punishmentGroups = [ - {'子', '卯'}, // 子卯刑 - {'子', '酉'}, // 子酉刑 - {'午', '酉'}, // 午酉刑 - {'午', '卯'}, // 午卯刑 - {'寅', '亥'}, // 寅亥刑 - {'巳', '申'}, // 巳申刑 - {'丑', '辰'}, // 丑辰刑 - {'寅', '巳', '申'}, // 寅巳申刑 - {'未', '戌', '丑'}, // 未戌丑刑 - ]; - Future exportToExcel(String processedContent, {String? fileName}) async { try { final excel = Excel.createExcel(); final sheet = excel['Sheet1']; - final lines = processedContent.split('\n'); - bool isHeaderProcessed = false; + final Map previousPalaces = {}; + // 1. 处理所有数据行 for (int rowIndex = 0; rowIndex < lines.length; rowIndex++) { final line = lines[rowIndex]; if (line.trim().isEmpty) continue; final cells = line.split(','); - // 处理标题行 if (cells.isNotEmpty && cells[0] == 'Date') { - // 添加新列标题 - final extendedCells = [...cells, '合', '冲', '刑']; - - // 写入扩展后的标题行 - for (int col = 0; col < extendedCells.length; col++) { - final cell = sheet.cell( - CellIndex.indexByColumnRow(columnIndex: col, rowIndex: rowIndex), - ); - cell.value = TextCellValue(extendedCells[col]); - - // 设置标题行背景色为浅绿色 - cell.cellStyle = CellStyle( - backgroundColorHex: ExcelColor.lightGreen, - fontColorHex: ExcelColor.black, - bold: true, - ); - } - - isHeaderProcessed = true; + _processHeaderRow(sheet, rowIndex, cells); continue; } // 处理数据行 - for (int col = 0; col < cells.length; col++) { - final cellValue = cells[col]; - final cell = sheet.cell(CellIndex.indexByColumnRow(columnIndex: col, rowIndex: rowIndex)); - cell.value = TextCellValue(cellValue); - - // 应用红色格式到7-14列(索引6-13) - if (col >= 6 && col <= 13) { - _applyConditionalFormatting(cell, cellValue); - } - } - - // 如果是数据行且已处理过标题行,计算关系 - if (isHeaderProcessed && rowIndex > 0) { - _calculateRelationships(sheet, rowIndex, cells); + int outputCol = 0; + + // 1.1 处理前四列数据 + for (int col = 0; col < 4 && col < cells.length; col++) { + sheet.cell(CellIndex.indexByColumnRow( + columnIndex: outputCol, + rowIndex: rowIndex + )).value = TextCellValue(cells[col]); + outputCol++; } - } - - // 设置列宽 - for (var i = 1; i < 18; i++) { - sheet.setColumnWidth(i, 8.0); - } - final bytes = excel.save()!; - - // 显示保存对话框 - final String? outputPath = await FilePicker.platform.saveFile( - dialogTitle: 'Save Excel File', - fileName: fileName ?? 'pdf_export_${DateTime.now().millisecondsSinceEpoch}.xlsx', - allowedExtensions: ['xlsx'], - type: FileType.custom, - ); + // 1.2 处理星宿列数据 + _adjustmentService.processRowData( + cells, + previousPalaces, + sheet, + rowIndex, + outputCol + ); + } - if (outputPath == null) { - Logger().info('User cancelled Excel export'); - return; - } + // 2. 单独处理关系计算(统一处理整个sheet) + _relationshipService.processEntireSheet(sheet); - await File(outputPath).writeAsBytes(bytes); - Logger().info('文件已导出至: $outputPath'); + _setColumnWidths(sheet); + await _saveExcelFile(excel, fileName); } catch (e) { Logger().error('[$_logTag] Excel export失败: ${e.toString()}'); rethrow; } } - void _calculateRelationships(Sheet sheet, int rowIndex, List cells) { - final List validCells = []; - - // 1. Extract valid cells (columns 5-18, excluding 15-16) with degree, zodiac and minute - for (int col = 4; col <= 17; col++) { - if (col == 14 || col == 15) continue; // Skip South/North columns + void _processHeaderRow(Sheet sheet, int rowIndex, List cells) { + final extendedCells = []; - if (col < cells.length) { - final data = _parseCellData(cells[col]); - if (data != null && data.isValid()) { - validCells.add(data..columnIndex = col); - } - } + // 1. 添加前四列标题 + for (int col = 0; col < 4 && col < cells.length; col++) { + extendedCells.add(cells[col]); } - final List heRelations = []; // 合关系 - final List chongRelations = []; // 冲关系 - final List xingRelations = []; // 刑关系 - - // 2. Compare all valid cells pairwise - for (int i = 0; i < validCells.length; i++) { - for (int j = i + 1; j < validCells.length; j++) { - final cell1 = validCells[i]; - final cell2 = validCells[j]; - - // Get column titles - final title1 = _columnTitles[cell1.columnIndex] ?? ''; - final title2 = _columnTitles[cell2.columnIndex] ?? ''; - - // Check for harmony (合) - if (_isHarmony(cell1, cell2)) { - heRelations.add('$title1$title2合'); - } - - // Check for conflict (冲) - if (_isConflict(cell1.zodiac, cell2.zodiac)) { - chongRelations.add('$title1$title2冲'); - } - - // Check for punishment (刑) - if (_isPunishment(cell1.zodiac, cell2.zodiac)) { - xingRelations.add('$title1$title2刑'); - } - } + // 2. 添加5-18列的宫位标题和原始标题 + for (int col = 4; col <= 17 && col < cells.length; col++) { + final title = _columnTitles[col] ?? ''; + extendedCells.add('${title}宫'); + extendedCells.add(cells[col]); } - // 3. 写入关系列 (第19-21列) - if (heRelations.isNotEmpty) { - sheet - .cell(CellIndex.indexByColumnRow(columnIndex: cells.length, rowIndex: rowIndex)) - .value = TextCellValue(heRelations.join(' ')); - } - if (chongRelations.isNotEmpty) { - sheet - .cell(CellIndex.indexByColumnRow(columnIndex: cells.length + 1, rowIndex: rowIndex)) - .value = TextCellValue(chongRelations.join(' ')); - } - if (xingRelations.isNotEmpty) { - sheet - .cell(CellIndex.indexByColumnRow(columnIndex: cells.length + 2, rowIndex: rowIndex)) - .value = TextCellValue(xingRelations.join(' ')); - } - } - - CellData? _parseCellData(String cellValue) { - // 支持两种格式: - // 1. 度地支分(如"12酉34") - // 2. 度地支分'秒(如"10丑48'49"或"11°50'00") - - final match = RegExp( - r"^(\d+)([^\d\s°\']+)(\d+)(?:[\'](\d{1,2}))?$", - ).firstMatch(cellValue.trim()); - if (match == null) return null; - - final degreeStr = match.group(1); - final zodiac = match.group(2); - final minuteStr = match.group(3); - final secondStr = match.group(4); // 可能为null - - if (degreeStr == null || zodiac == null || minuteStr == null) return null; - - // 转换度分秒为double类型的度 - final degree = double.tryParse(degreeStr); - final minute = double.tryParse(minuteStr); - final second = secondStr != null ? double.tryParse(secondStr) : 0.0; - - if (degree == null || minute == null) return null; - - // 将秒转换为分钟的小数部分(60秒=1分钟) - final totalMinutes = minute + (second ?? 0) / 60; - - return CellData(degree: degree, zodiac: zodiac, minute: totalMinutes); - } - - bool _isHarmony(CellData cell1, CellData cell2) { - // 排除相同地支 - if (cell1.zodiac == cell2.zodiac) return false; - - // 1. 先检查度数差是否在1度以内 - final totalDiff = (cell1.totalMinutes - cell2.totalMinutes).abs(); - final isDegreeMatch = totalDiff <= 60; - if (isDegreeMatch) { - // 如果度数差在1度以内,直接返回true - return true; - } - - // 2. 检查地支是否在合局中r - var isZodiacMatch = false; - for (final group in harmonyGroups) { - if (group.contains(cell1.zodiac) && group.contains(cell2.zodiac)) { - isZodiacMatch = true; - break; - } - } - - // 两种条件满足其一即可 - return isDegreeMatch || isZodiacMatch; - } - - bool _isConflict(String zodiac1, String zodiac2) { - // 排除相同地支 - if (zodiac1 == zodiac2) return false; - - for (final group in conflictGroups) { - if (group.contains(zodiac1) && group.contains(zodiac2)) { - return true; - } + // 3. 添加关系列标题 + extendedCells.addAll(['合', '冲', '刑']); + + // 写入所有标题 + for (int col = 0; col < extendedCells.length; col++) { + final cell = sheet.cell(CellIndex.indexByColumnRow(columnIndex: col, rowIndex: rowIndex)); + cell.value = TextCellValue(extendedCells[col]); + cell.cellStyle = CellStyle( + backgroundColorHex: ExcelColor.lightGreen, + fontColorHex: ExcelColor.black, + bold: true, + ); } - return false; } - bool _isPunishment(String zodiac1, String zodiac2) { - // 排除相同地支 - if (zodiac1 == zodiac2) return false; - - for (final group in punishmentGroups) { - if (group.contains(zodiac1) && group.contains(zodiac2)) { - return true; - } + void _setColumnWidths(Sheet sheet) { + // 设置所有列宽(前四列 + 宫位和数据列 + 关系列) + for (var i = 0; i < 4 + (_columnTitles.length * 2) + 3; i++) { + sheet.setColumnWidth(i, 8.0); } - return false; } - // 跟踪每列的红色区域状态 - final List _columnRedStates = List.filled(14, false); - - void _applyConditionalFormatting(Data cell, String value) { - if (value.contains('R')) { - _columnRedStates[cell.cellIndex.columnIndex] = true; - } else if (value.contains('D')) { - _columnRedStates[cell.cellIndex.columnIndex] = false; - } + Future _saveExcelFile(Excel excel, String? fileName) async { + final bytes = excel.save()!; + final outputPath = await FilePicker.platform.saveFile( + dialogTitle: 'Save Excel File', + fileName: fileName ?? 'pdf_export_${DateTime.now().millisecondsSinceEpoch}.xlsx', + allowedExtensions: ['xlsx'], + type: FileType.custom, + ); - if (_columnRedStates[cell.cellIndex.columnIndex]) { - cell.cellStyle = CellStyle(fontColorHex: ExcelColor.red); + if (outputPath != null) { + await File(outputPath).writeAsBytes(bytes); + Logger().info('文件已导出至: $outputPath'); + } else { + Logger().info('User cancelled Excel export'); } } } - -class CellData { - final double? degree; - final String zodiac; - final double? minute; - int columnIndex = -1; - - // 定义有效的地支集合 - static const validZodiacs = {'亥', '戌', '酉', '申', '未', '午', '巳', '辰', '卯', '寅', '丑', '子'}; - - CellData({this.degree, required this.zodiac, this.minute}); - - bool isValid() { - return degree != null && minute != null && validZodiacs.contains(zodiac); - } - - double get totalMinutes => (degree ?? 0) * 60 + (minute ?? 0); -} diff --git a/win_text_editor/lib/modules/pdf_parse/services/relationship_calculator_service.dart b/win_text_editor/lib/modules/pdf_parse/services/relationship_calculator_service.dart new file mode 100644 index 0000000..114b767 --- /dev/null +++ b/win_text_editor/lib/modules/pdf_parse/services/relationship_calculator_service.dart @@ -0,0 +1,169 @@ +import 'package:excel/excel.dart'; + +class RelationshipCalculatorService { + final Map _columnRedStates = {}; + final List> _aspects = []; // Stores [conjunction, opposition, square] for each row + + static const List zodiacColumns = [ + '日', + '月', + '水', + '金', + '火', + '木', + '土', + '天', + '海', + '冥', + '南', + '北', + '孛', + '凯', + ]; + + void processEntireSheet(Sheet sheet) { + _columnRedStates.clear(); + _aspects.clear(); + + for (var rowIndex = 0; rowIndex < sheet.maxRows; rowIndex++) { + final row = sheet.rows[rowIndex]; + if (row.isEmpty) continue; + + // 跳过所有第一个单元格值为"Date"的行 + if (row.first?.value?.toString().startsWith('Date') == true) { + continue; + } + + _processZodiacColumns(rowIndex, row); + _calculateCoordinateRelationships(row); + } + + // Add aspect markers to the last three rows + _addAspectMarkers(sheet); + } + + void _processZodiacColumns(int rowIndex, List row) { + for (int i = 0; i < zodiacColumns.length; i++) { + final columnTitle = zodiacColumns[i]; + final dataColumnIndex = 4 + (i * 2) + 1; + + if (dataColumnIndex >= row.length) continue; + + final cell = row[dataColumnIndex]; + if (cell == null) continue; + + final cellValue = cell.value?.toString() ?? ''; + + if (cellValue.contains('R')) { + cell.cellStyle = CellStyle(fontColorHex: ExcelColor.red); + _columnRedStates[columnTitle] = true; + } else if (cellValue.contains('D')) { + cell.cellStyle = CellStyle(fontColorHex: ExcelColor.black); + _columnRedStates[columnTitle] = false; + } else if (_columnRedStates[columnTitle] == true) { + cell.cellStyle = CellStyle(fontColorHex: ExcelColor.red); + } else { + cell.cellStyle = CellStyle(fontColorHex: ExcelColor.black); + } + } + } + + void _calculateCoordinateRelationships(List row) { + List positionsInMinutes = []; + List planets = []; + + // Extract positions for all planets in minutes + for (int i = 0; i < zodiacColumns.length; i++) { + final dataColumnIndex = 4 + (i * 2) + 1; + if (dataColumnIndex >= row.length) continue; + + final cell = row[dataColumnIndex]; + if (cell == null) continue; + + final cellValue = cell.value?.toString() ?? ''; + final cleanedValue = cellValue.replaceAll(RegExp(r'[RD]'), ''); + if (cleanedValue.isEmpty) continue; + + try { + final parts = cleanedValue.split('°'); + if (parts.length != 2) continue; + + final degrees = int.parse(parts[0]); + final minutesPart = parts[1].split('\''); + final minutes = minutesPart.isNotEmpty ? int.parse(minutesPart[0]) : 0; + + positionsInMinutes.add(degrees * 60 + minutes); + planets.add(zodiacColumns[i]); + } catch (e) { + continue; + } + } + + // Calculate aspects + List conjunctions = []; + List oppositions = []; + List squares = []; + + for (int i = 0; i < positionsInMinutes.length; i++) { + for (int j = i + 1; j < positionsInMinutes.length; j++) { + final diff = (positionsInMinutes[i] - positionsInMinutes[j]).abs(); + final circularDiff = diff > 10800 ? 21600 - diff : diff; // 360° = 21600 minutes + + // Check for conjunction (<60 minutes or 120°±30 minutes) + if (circularDiff < 60 || (circularDiff >= 7170 && circularDiff <= 7290)) { + // 119°30' to 120°29' + + if (planets[i] == '南' && planets[j] == '北') { + // 南北合不用显示 + } else { + conjunctions.add('${planets[i]}${planets[j]}合'); + } + } + // Check for square (90°±30 minutes) + else if (circularDiff >= 5370 && circularDiff <= 5490) { + // 89°30' to 90°29' + squares.add('${planets[i]}${planets[j]}刑'); + } + // Check for opposition (180°±30 minutes) + else if (circularDiff >= 10770 && circularDiff <= 10890) { + // 179°30' to 180°29' + oppositions.add('${planets[i]}${planets[j]}冲'); + } + } + } + + _aspects.add([conjunctions.join(', '), oppositions.join(', '), squares.join(', ')]); + } + + void _addAspectMarkers(Sheet sheet) { + if (_aspects.isEmpty) return; + + // 确定要在哪三列添加数据(最后三列) + final firstAspectCol = sheet.maxColumns - 3; + final secondAspectCol = sheet.maxColumns - 2; + final thirdAspectCol = sheet.maxColumns - 1; + + for (int rowIndex = 0; rowIndex < sheet.maxRows; rowIndex++) { + final row = sheet.rows[rowIndex]; + if (row.isEmpty || row.first?.value?.toString().startsWith('Date') == true) { + continue; + } + + // 获取当前行对应的aspect数据 + int dataIndex = rowIndex - 1; // 假设第一行是标题行 + if (dataIndex >= 0 && dataIndex < _aspects.length) { + final aspects = _aspects[dataIndex]; + + _setCellValue(sheet, firstAspectCol, rowIndex, aspects[0]); + _setCellValue(sheet, secondAspectCol, rowIndex, aspects[1]); + _setCellValue(sheet, thirdAspectCol, rowIndex, aspects[2]); + } + } + } + + // 辅助方法:正确设置单元格值 + void _setCellValue(Sheet sheet, int col, int row, String value) { + final cell = sheet.cell(CellIndex.indexByColumnRow(columnIndex: col, rowIndex: row)); + cell.value = TextCellValue(value); // 使用TextCellValue包装字符串 + } +}