Skip to content

Commit

Permalink
Merge pull request #72 from kkebo/add-list-comparer
Browse files Browse the repository at this point in the history
  • Loading branch information
kkebo authored Dec 8, 2024
2 parents 33e6a83 + 5abe750 commit a9fd5d0
Show file tree
Hide file tree
Showing 10 changed files with 322 additions and 2 deletions.
11 changes: 10 additions & 1 deletion Package.resolved
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"originHash" : "5ea303bddc5af1e384e48dfef25fbc8ca7d9701203157c774b7cf0465f01924a",
"originHash" : "f15f53b6144697d7b925339ea00e99ab41dbf8b07def87b17988a5dabbc8d12a",
"pins" : [
{
"identity" : "codeeditorview",
Expand Down Expand Up @@ -64,6 +64,15 @@
"version" : "1.8.1"
}
},
{
"identity" : "swift-collections",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-collections",
"state" : {
"revision" : "671108c96644956dddcd89dd59c203dcdb36cec7",
"version" : "1.1.4"
}
},
{
"identity" : "swift-html-entities",
"kind" : "remoteSourceControl",
Expand Down
2 changes: 2 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ let package = Package(
.package(url: "https://github.com/JohnSundell/Ink", from: "0.6.0"),
.package(url: "https://github.com/kkebo/swift-uniyaml", branch: "origin/issues/1"),
.package(url: "https://github.com/mchakravarty/CodeEditorView", from: "0.13.0"),
.package(url: "https://github.com/apple/swift-collections", from: "1.1.4"),
],
targets: [
.executableTarget(
Expand All @@ -36,6 +37,7 @@ let package = Package(
.product(name: "Ink", package: "Ink"),
.product(name: "UniYAML", package: "swift-uniyaml"),
.product(name: "CodeEditorView", package: "CodeEditorView"),
.product(name: "OrderedCollections", package: "swift-collections"),
],
swiftSettings: [
.unsafeFlags(["-Xfrontend", "-warn-long-function-bodies=100"], .when(configuration: .debug)),
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ This app is a SwiftUI reimplementation of [DevToys](https://devtoys.app), a Swis
- [ ] Regex Tester
- [ ] Text Comparer
- [x] Markdown Preview
- [x] List Comparer
- Graphic
- [ ] Color Blindness Simulator
- [ ] PNG / JPEG Compressor
Expand Down
1 change: 1 addition & 0 deletions Sources/AppState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ final class AppState {
let markdownPreviewViewState = MarkdownPreviewViewState()
let timestampConverterViewState = TimestampConverterViewState()
let jsonYAMLConverterViewState = JSONYAMLConverterViewState()
let listComparerViewState = ListComparerViewState()
}
170 changes: 170 additions & 0 deletions Sources/Models/Text/ListComparer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import OrderedCollections

private struct CaseInsensitiveString<S: StringProtocol> {
var value: S
}

extension CaseInsensitiveString: Hashable {}

extension CaseInsensitiveString: Equatable {
static func == (lhs: Self, rhs: Self) -> Bool {
lhs.value.caseInsensitiveCompare(rhs.value) == .orderedSame
}
}

struct ListComparer {
static func compare(a: String, b: String, caseSensitive: Bool, mode: ListComparisonMode) -> String {
switch mode {
case .intersection: Self.intersection(a: a, b: b, caseSensitive: caseSensitive)
case .union: Self.union(a: a, b: b, caseSensitive: caseSensitive)
case .aOnly: Self.difference(a: a, b: b, caseSensitive: caseSensitive)
case .bOnly: Self.difference(a: b, b: a, caseSensitive: caseSensitive)
}
}

private static func intersection(a: String, b: String, caseSensitive: Bool) -> String {
if caseSensitive {
OrderedSet(a.split(whereSeparator: \.isNewline))
.intersection(b.split(whereSeparator: \.isNewline))
.joined(separator: "\n")
} else {
OrderedSet(a.split(whereSeparator: \.isNewline).map(CaseInsensitiveString.init))
.intersection(b.split(whereSeparator: \.isNewline).map(CaseInsensitiveString.init))
.lazy
.map(\.value)
.joined(separator: "\n")
}
}

private static func union(a: String, b: String, caseSensitive: Bool) -> String {
if caseSensitive {
OrderedSet(a.split(whereSeparator: \.isNewline))
.union(b.split(whereSeparator: \.isNewline))
.joined(separator: "\n")
} else {
OrderedSet(a.split(whereSeparator: \.isNewline).map(CaseInsensitiveString.init))
.union(b.split(whereSeparator: \.isNewline).map(CaseInsensitiveString.init))
.lazy
.map(\.value)
.joined(separator: "\n")
}
}

private static func difference(a: String, b: String, caseSensitive: Bool) -> String {
if caseSensitive {
OrderedSet(a.split(whereSeparator: \.isNewline))
.subtracting(b.split(whereSeparator: \.isNewline))
.joined(separator: "\n")
} else {
OrderedSet(a.split(whereSeparator: \.isNewline).map(CaseInsensitiveString.init))
.subtracting(b.split(whereSeparator: \.isNewline).map(CaseInsensitiveString.init))
.lazy
.map(\.value)
.joined(separator: "\n")
}
}
}

#if TESTING_ENABLED
import Foundation
import PlaygroundTester

@objcMembers
final class ListComparerTests: TestCase {
func testIntersectionCaseInsensitive() {
func compare(a: String, b: String) -> String {
ListComparer.compare(a: a, b: b, caseSensitive: false, mode: .intersection)
}
AssertEqual("", other: compare(a: "a\nB\nc", b: "d\nE\nf"))
AssertEqual("c", other: compare(a: "a\nB\nc", b: "c\nE\nf"))
AssertEqual("B", other: compare(a: "a\nB\nc", b: "b\nE\nf"))
AssertEqual("", other: compare(a: "a\nB\nc\nb", b: "d\nE\nf"))
AssertEqual("", other: compare(a: "a\nB\nc", b: ""))
AssertEqual("", other: compare(a: "", b: "d\nE\nf"))
}

func testIntersectionCaseSensitive() {
func compare(a: String, b: String) -> String {
ListComparer.compare(a: a, b: b, caseSensitive: true, mode: .intersection)
}
AssertEqual("", other: compare(a: "a\nB\nc", b: "d\nE\nf"))
AssertEqual("c", other: compare(a: "a\nB\nc", b: "c\nE\nf"))
AssertEqual("", other: compare(a: "a\nB\nc", b: "b\nE\nf"))
AssertEqual("", other: compare(a: "a\nB\nc\nb", b: "d\nE\nf"))
AssertEqual("", other: compare(a: "a\nB\nc", b: ""))
AssertEqual("", other: compare(a: "", b: "d\nE\nf"))
}

func testUnionCaseInsensitive() {
func compare(a: String, b: String) -> String {
ListComparer.compare(a: a, b: b, caseSensitive: false, mode: .union)
}
AssertEqual("a\nB\nc\nd\nE\nf", other: compare(a: "a\nB\nc", b: "d\nE\nf"))
AssertEqual("a\nB\nc\nE\nf", other: compare(a: "a\nB\nc", b: "c\nE\nf"))
AssertEqual("a\nB\nc\nE\nf", other: compare(a: "a\nB\nc", b: "b\nE\nf"))
AssertEqual("a\nB\nc\nd\nE\nf", other: compare(a: "a\nB\nc\nb", b: "d\nE\nf"))
AssertEqual("a\nB\nc", other: compare(a: "a\nB\nc", b: ""))
AssertEqual("d\nE\nf", other: compare(a: "", b: "d\nE\nf"))
}

func testUnionCaseSensitive() {
func compare(a: String, b: String) -> String {
ListComparer.compare(a: a, b: b, caseSensitive: true, mode: .union)
}
AssertEqual("a\nB\nc\nd\nE\nf", other: compare(a: "a\nB\nc", b: "d\nE\nf"))
AssertEqual("a\nB\nc\nE\nf", other: compare(a: "a\nB\nc", b: "c\nE\nf"))
AssertEqual("a\nB\nc\nb\nE\nf", other: compare(a: "a\nB\nc", b: "b\nE\nf"))
AssertEqual("a\nB\nc\nb\nd\nE\nf", other: compare(a: "a\nB\nc\nb", b: "d\nE\nf"))
AssertEqual("a\nB\nc", other: compare(a: "a\nB\nc", b: ""))
AssertEqual("d\nE\nf", other: compare(a: "", b: "d\nE\nf"))
}

func testAOnlyCaseInsensitive() {
func compare(a: String, b: String) -> String {
ListComparer.compare(a: a, b: b, caseSensitive: false, mode: .aOnly)
}
AssertEqual("a\nB\nc", other: compare(a: "a\nB\nc", b: "d\nE\nf"))
AssertEqual("a\nB", other: compare(a: "a\nB\nc", b: "c\nE\nf"))
AssertEqual("a\nc", other: compare(a: "a\nB\nc", b: "b\nE\nf"))
AssertEqual("a\nB\nc", other: compare(a: "a\nB\nc\nb", b: "d\nE\nf"))
AssertEqual("a\nB\nc", other: compare(a: "a\nB\nc", b: ""))
AssertEqual("", other: compare(a: "", b: "d\nE\nf"))
}

func testAOnlyCaseSensitive() {
func compare(a: String, b: String) -> String {
ListComparer.compare(a: a, b: b, caseSensitive: true, mode: .aOnly)
}
AssertEqual("a\nB\nc", other: compare(a: "a\nB\nc", b: "d\nE\nf"))
AssertEqual("a\nB", other: compare(a: "a\nB\nc", b: "c\nE\nf"))
AssertEqual("a\nB\nc", other: compare(a: "a\nB\nc", b: "b\nE\nf"))
AssertEqual("a\nB\nc\nb", other: compare(a: "a\nB\nc\nb", b: "d\nE\nf"))
AssertEqual("a\nB\nc", other: compare(a: "a\nB\nc", b: ""))
AssertEqual("", other: compare(a: "", b: "d\nE\nf"))
}

func testBOnlyCaseInsensitive() {
func compare(a: String, b: String) -> String {
ListComparer.compare(a: a, b: b, caseSensitive: false, mode: .bOnly)
}
AssertEqual("d\nE\nf", other: compare(a: "a\nB\nc", b: "d\nE\nf"))
AssertEqual("E\nf", other: compare(a: "a\nB\nc", b: "c\nE\nf"))
AssertEqual("E\nf", other: compare(a: "a\nB\nc", b: "b\nE\nf"))
AssertEqual("d\nE\nf", other: compare(a: "a\nB\nc\nb", b: "d\nE\nf"))
AssertEqual("", other: compare(a: "a\nB\nc", b: ""))
AssertEqual("d\nE\nf", other: compare(a: "", b: "d\nE\nf"))
}

func testBOnlyCaseSensitive() {
func compare(a: String, b: String) -> String {
ListComparer.compare(a: a, b: b, caseSensitive: true, mode: .bOnly)
}
AssertEqual("d\nE\nf", other: compare(a: "a\nB\nc", b: "d\nE\nf"))
AssertEqual("E\nf", other: compare(a: "a\nB\nc", b: "c\nE\nf"))
AssertEqual("b\nE\nf", other: compare(a: "a\nB\nc", b: "b\nE\nf"))
AssertEqual("d\nE\nf", other: compare(a: "a\nB\nc\nb", b: "d\nE\nf"))
AssertEqual("", other: compare(a: "a\nB\nc", b: ""))
AssertEqual("d\nE\nf", other: compare(a: "", b: "d\nE\nf"))
}
}
#endif
19 changes: 19 additions & 0 deletions Sources/Models/Text/ListComparisonMode.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
enum ListComparisonMode {
case intersection
case union
case aOnly
case bOnly
}

extension ListComparisonMode: CaseIterable {}

extension ListComparisonMode: CustomStringConvertible {
var description: String {
switch self {
case .intersection: "A ∩ B"
case .union: "A ∪ B"
case .aOnly: "A Only"
case .bOnly: "B Only"
}
}
}
77 changes: 77 additions & 0 deletions Sources/Pages/Text/ListComparerView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import SwiftUI

struct ListComparerView {
@Environment(\.horizontalSizeClass) private var hSizeClass
@Bindable var state: ListComparerViewState

init(state: AppState) {
self.state = state.listComparerViewState
}
}

extension ListComparerView: View {
var body: some View {
ToyPage {
ToySection("Configuration") {
ConfigurationRow("Case sensitive comparison", systemImage: "textformat") {
Toggle("", isOn: self.$state.isCaseSensitive).labelsHidden()
}
ConfigurationRow("Comparison mode", systemImage: "brain") {
Picker("", selection: self.$state.comparisonMode) {
ForEach(ListComparisonMode.allCases, id: \.self) {
Text(LocalizedStringKey($0.description))
}
}
.labelsHidden()
}
}

if self.hSizeClass == .compact {
self.sectionA
self.sectionB
} else {
HStack {
self.sectionA
Divider()
self.sectionB
}
}

ToySection(LocalizedStringKey(self.state.comparisonMode.description)) {
CopyButton(text: self.state.output)
} content: {
CodeEditor(text: .constant(self.state.output))
}
}
.navigationTitle(Tool.listComparer.strings.localizedLongTitle)
}

private var sectionA: some View {
ToySection("A") {
PasteButton(text: self.$state.a)
OpenFileButton(text: self.$state.a)
ClearButton(text: self.$state.a)
} content: {
CodeEditor(text: self.$state.a)
}
}

private var sectionB: some View {
ToySection("B") {
PasteButton(text: self.$state.b)
OpenFileButton(text: self.$state.b)
ClearButton(text: self.$state.b)
} content: {
CodeEditor(text: self.$state.b)
}
}
}

struct ListComparerView_Previews: PreviewProvider {
static var previews: some View {
NavigationStack {
ListComparerView(state: .init())
}
.previewPresets()
}
}
27 changes: 27 additions & 0 deletions Sources/Pages/Text/ListComparerViewState.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import Observation

@Observable
final class ListComparerViewState {
var comparisonMode = ListComparisonMode.intersection {
didSet { self.didUpdate() }
}
var isCaseSensitive = false {
didSet { self.didUpdate() }
}
var a = "" {
didSet { self.didUpdate() }
}
var b = "" {
didSet { self.didUpdate() }
}
private(set) var output = ""

private func didUpdate() {
self.output = ListComparer.compare(
a: self.a,
b: self.b,
caseSensitive: self.isCaseSensitive,
mode: self.comparisonMode
)
}
}
Loading

0 comments on commit a9fd5d0

Please sign in to comment.