-
-
Notifications
You must be signed in to change notification settings - Fork 337
feat(inspector): split and merge columns in the CSV editor #1941
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,6 +17,18 @@ final class CSVRowStore { | |
| case remove(index: Int) | ||
| } | ||
|
|
||
| enum SplitSpec { | ||
| case literal(String) | ||
| case regex(NSRegularExpression) | ||
| } | ||
|
|
||
| struct StoreState { | ||
| let columnNames: [String] | ||
| let headerRef: RowRef | ||
| let logicalRows: [RowRef] | ||
| let columnTransforms: [ColumnTransform] | ||
| } | ||
|
|
||
| struct Snapshot: InspectorDataSnapshot { | ||
| let data: Data | ||
| let parser: CSVStreamingParser | ||
|
|
@@ -269,6 +281,94 @@ final class CSVRowStore { | |
| return previous | ||
| } | ||
|
|
||
| func splitColumn(at index: Int, spec: SplitSpec) { | ||
| guard index >= 0, index < columnNames.count else { return } | ||
| let baseName = columnNames[index] | ||
| var rowCells: [[String]] = [] | ||
| var pieceRows: [[String]] = [] | ||
| rowCells.reserveCapacity(logicalRows.count) | ||
| pieceRows.reserveCapacity(logicalRows.count) | ||
| var pieceCount = 1 | ||
| for row in logicalRows.indices { | ||
| let cells = cells(forRow: row) | ||
| let value = index < cells.count ? cells[index] : "" | ||
| let pieces = Self.split(value, spec: spec) | ||
| pieceCount = max(pieceCount, pieces.count) | ||
| rowCells.append(cells) | ||
| pieceRows.append(pieces) | ||
| } | ||
| let newNames = (0..<pieceCount).map { "\(baseName) \($0 + 1)" } | ||
| for row in logicalRows.indices { | ||
| var cells = rowCells[row] | ||
| let pieces = pieceRows[row] | ||
| let padded = (0..<pieceCount).map { $0 < pieces.count ? pieces[$0] : "" } | ||
| if index < cells.count { cells.remove(at: index) } | ||
| cells.insert(contentsOf: padded, at: min(index, cells.count)) | ||
| logicalRows[row] = .materialized(cells) | ||
| } | ||
| columnNames.remove(at: index) | ||
| columnNames.insert(contentsOf: newNames, at: index) | ||
| finishStructuralRewrite() | ||
| } | ||
|
|
||
| func mergeColumns(at index: Int, separator: String) { | ||
| guard index >= 0, index + 1 < columnNames.count else { return } | ||
| for row in logicalRows.indices { | ||
| var cells = cells(forRow: row) | ||
| while cells.count <= index + 1 { cells.append("") } | ||
| cells[index] = cells[index] + separator + cells[index + 1] | ||
| cells.remove(at: index + 1) | ||
| logicalRows[row] = .materialized(cells) | ||
| } | ||
| columnNames.remove(at: index + 1) | ||
| finishStructuralRewrite() | ||
| } | ||
|
|
||
| func captureState() -> StoreState { | ||
| StoreState( | ||
| columnNames: columnNames, | ||
| headerRef: headerRef, | ||
| logicalRows: logicalRows, | ||
| columnTransforms: columnTransforms | ||
| ) | ||
| } | ||
|
|
||
| func restore(_ state: StoreState) { | ||
| columnNames = state.columnNames | ||
| headerRef = state.headerRef | ||
| logicalRows = state.logicalRows | ||
| columnTransforms = state.columnTransforms | ||
| cache.removeAll() | ||
| cacheOrder.removeAll() | ||
| } | ||
|
|
||
| static func split(_ value: String, spec: SplitSpec) -> [String] { | ||
| switch spec { | ||
| case .literal(let separator): | ||
| guard !separator.isEmpty else { return [value] } | ||
| return value.components(separatedBy: separator) | ||
| case .regex(let regex): | ||
| let text = value as NSString | ||
| guard text.length > 0 else { return [value] } | ||
| var pieces: [String] = [] | ||
| var lastEnd = 0 | ||
| regex.enumerateMatches(in: value, range: NSRange(location: 0, length: text.length)) { match, _, _ in | ||
| guard let match, match.range.length > 0 else { return } | ||
|
Comment on lines
+355
to
+356
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When users choose Regex mode with a valid zero-width separator such as Useful? React with 👍 / 👎. |
||
| pieces.append(text.substring(with: NSRange(location: lastEnd, length: match.range.location - lastEnd))) | ||
| lastEnd = match.range.location + match.range.length | ||
| } | ||
| pieces.append(text.substring(from: lastEnd)) | ||
| return pieces | ||
| } | ||
| } | ||
|
|
||
| private func finishStructuralRewrite() { | ||
| headerRef = .materialized(columnNames) | ||
| columnTransforms = [] | ||
| cache.removeAll() | ||
| cacheOrder.removeAll() | ||
| } | ||
|
|
||
| private func applyColumnTransforms(_ cells: [String]) -> [String] { | ||
| Self.applyColumnTransforms(cells, transforms: columnTransforms) | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A split or merge invokes this method and clears every manual type override, including overrides on columns unrelated to the operation. For example, splitting column A removes a user-selected type for column C and can change its subsequent sort behavior; retain unaffected overrides and shift their indices while replacing only the structurally affected ones.
Useful? React with 👍 / 👎.