diff --git a/CHANGELOG.md b/CHANGELOG.md index 502dde331..b71e29656 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ ### Other Changes * Added the `Waypoint.layer` property, which can ensure that the route begins on the correct road if it is above or below another road. ([#745](https://github.com/mapbox/mapbox-directions-swift/pull/745)) +* Expanded `AttributeOptions` to allow user options with custom values. See `AttributeOptions.customOptionsByRawValue` for reference. ([#748](https://github.com/mapbox/mapbox-directions-swift/pull/748)) * Fixed incorrect shape indicies in `RouteLeg.incidents` after route refresh. ([#752](https://github.com/mapbox/mapbox-directions-swift/pull/752)) ## 2.7.0 diff --git a/MapboxDirections.xcodeproj/project.pbxproj b/MapboxDirections.xcodeproj/project.pbxproj index 60f19bbca..afe9bd664 100644 --- a/MapboxDirections.xcodeproj/project.pbxproj +++ b/MapboxDirections.xcodeproj/project.pbxproj @@ -14,6 +14,16 @@ 2B28E22527EDB2AA0029E4C1 /* ForeignMemberContainerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B28E22227EDB2A90029E4C1 /* ForeignMemberContainerTests.swift */; }; 2B39DD40270F034700ED68E4 /* CodingOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B39DD3F270F034700ED68E4 /* CodingOperation.swift */; }; 2B4383022549C22700A3E38B /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B4383002549C22700A3E38B /* main.swift */; }; + 2B46024528EDB1670008A624 /* CustomValueOptionSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B46024428EDB1670008A624 /* CustomValueOptionSet.swift */; }; + 2B46024628EDB1670008A624 /* CustomValueOptionSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B46024428EDB1670008A624 /* CustomValueOptionSet.swift */; }; + 2B46024728EDB1670008A624 /* CustomValueOptionSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B46024428EDB1670008A624 /* CustomValueOptionSet.swift */; }; + 2B46024828EDB1670008A624 /* CustomValueOptionSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B46024428EDB1670008A624 /* CustomValueOptionSet.swift */; }; + 2B46025328EDB62E0008A624 /* AttributeOptionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B46024A28EDB18B0008A624 /* AttributeOptionsTests.swift */; }; + 2B46025428EDB62F0008A624 /* AttributeOptionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B46024A28EDB18B0008A624 /* AttributeOptionsTests.swift */; }; + 2B46025528EDB6300008A624 /* AttributeOptionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B46024A28EDB18B0008A624 /* AttributeOptionsTests.swift */; }; + 2B46025628EDB6390008A624 /* CustomStringOptionSetTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B46024928EDB18B0008A624 /* CustomStringOptionSetTests.swift */; }; + 2B46025728EDB63A0008A624 /* CustomStringOptionSetTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B46024928EDB18B0008A624 /* CustomStringOptionSetTests.swift */; }; + 2B46025828EDB63B0008A624 /* CustomStringOptionSetTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B46024928EDB18B0008A624 /* CustomStringOptionSetTests.swift */; }; 2B46DB0C27EDF8580068C893 /* RouteRefreshResponseWithForeignMembers.json in Resources */ = {isa = PBXBuildFile; fileRef = 2B46DB0B27EDF8580068C893 /* RouteRefreshResponseWithForeignMembers.json */; }; 2B46DB0D27EDF8580068C893 /* RouteRefreshResponseWithForeignMembers.json in Resources */ = {isa = PBXBuildFile; fileRef = 2B46DB0B27EDF8580068C893 /* RouteRefreshResponseWithForeignMembers.json */; }; 2B46DB0E27EDF8580068C893 /* RouteRefreshResponseWithForeignMembers.json in Resources */ = {isa = PBXBuildFile; fileRef = 2B46DB0B27EDF8580068C893 /* RouteRefreshResponseWithForeignMembers.json */; }; @@ -495,6 +505,9 @@ 2B28E22227EDB2A90029E4C1 /* ForeignMemberContainerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForeignMemberContainerTests.swift; sourceTree = ""; }; 2B39DD3F270F034700ED68E4 /* CodingOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = CodingOperation.swift; path = Sources/MapboxDirectionsCLI/CodingOperation.swift; sourceTree = ""; }; 2B4383002549C22700A3E38B /* main.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = main.swift; path = Sources/MapboxDirectionsCLI/main.swift; sourceTree = ""; }; + 2B46024428EDB1670008A624 /* CustomValueOptionSet.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomValueOptionSet.swift; sourceTree = ""; }; + 2B46024928EDB18B0008A624 /* CustomStringOptionSetTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomStringOptionSetTests.swift; sourceTree = ""; }; + 2B46024A28EDB18B0008A624 /* AttributeOptionsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttributeOptionsTests.swift; sourceTree = ""; }; 2B46DB0B27EDF8580068C893 /* RouteRefreshResponseWithForeignMembers.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = RouteRefreshResponseWithForeignMembers.json; sourceTree = ""; }; 2B5407EC2451B17E006C820B /* RouteRefreshResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouteRefreshResponse.swift; sourceTree = ""; }; 2B5407F12452FA8C006C820B /* RefreshedRoute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshedRoute.swift; sourceTree = ""; }; @@ -835,6 +848,7 @@ C51538CB1E807FF00093FF3E /* AttributeOptions.swift */, C58EA7A91E9D7EAD008F98CE /* Congestion.swift */, 2B9F387F272AE23A001DBA12 /* Credentials.swift */, + 2B46024428EDB1670008A624 /* CustomValueOptionSet.swift */, DD6254731AE70CB700017857 /* Directions.swift */, 4392557523440EC2006EEE88 /* DirectionsError.swift */, C59094BE203B800300EB2417 /* DirectionsOptions.swift */, @@ -885,7 +899,9 @@ children = ( DA6C9DAD1CAEC93800094FBC /* Fixtures */, C5247D701E818A24004B6154 /* AnnotationTests.swift */, + 2B46024A28EDB18B0008A624 /* AttributeOptionsTests.swift */, 43D992FB2437B8D2008A2D74 /* CredentialsTests.swift */, + 2B46024928EDB18B0008A624 /* CustomStringOptionSetTests.swift */, DAD06E34239F0B19001A917D /* DirectionsErrorTests.swift */, DA1A110A1D01045E009F82FA /* DirectionsTests.swift */, DA6C9DB11CAECA0E00094FBC /* Fixture.swift */, @@ -1450,6 +1466,7 @@ C53A02261E92C26E009837BD /* AttributeOptions.swift in Sources */, DA1A10C91D00F969009F82FA /* RouteLeg.swift in Sources */, C52552BA1FA15D5E00B1545C /* VisualInstructionBanner.swift in Sources */, + 2B46024628EDB1670008A624 /* CustomValueOptionSet.swift in Sources */, 35828C9F217A003F00ED546E /* OfflineDirections.swift in Sources */, 2B5F0E01273BEB3600CC2C1A /* RoadClassExclusionViolation.swift in Sources */, DAE7EA95230B5FD10003B211 /* Measurement.swift in Sources */, @@ -1517,6 +1534,7 @@ DA8F3A7323B56D3B00B56786 /* RouteLegTests.swift in Sources */, DA1A10CD1D00F972009F82FA /* V5Tests.swift in Sources */, DAE33A1C1F215DF600C06039 /* IntersectionTests.swift in Sources */, + 2B46025428EDB62F0008A624 /* AttributeOptionsTests.swift in Sources */, 2B5407FD245B070A006C820B /* RouteRefreshTests.swift in Sources */, C5DAACB0201AA92B001F9261 /* MatchTests.swift in Sources */, DA1A10CE1D00F972009F82FA /* Fixture.swift in Sources */, @@ -1527,6 +1545,7 @@ DA4F84EE21C08BFB008A0434 /* WaypointTests.swift in Sources */, DAABF78F2395ABA900CEEB61 /* SpokenInstructionTests.swift in Sources */, 35CC310C2285739700EA1966 /* WalkingOptionsTests.swift in Sources */, + 2B46025728EDB63A0008A624 /* CustomStringOptionSetTests.swift in Sources */, DABE6C7F236A37E200D370F4 /* JSONSerialization.swift in Sources */, DAABF7932395AE9800CEEB61 /* GeoJSONTests.swift in Sources */, F4D785F01DDD82C100FF4665 /* RouteStepTests.swift in Sources */, @@ -1547,6 +1566,7 @@ C53A02271E92C26F009837BD /* AttributeOptions.swift in Sources */, DA1A10EF1D010247009F82FA /* RouteLeg.swift in Sources */, C52552BB1FA15D5F00B1545C /* VisualInstructionBanner.swift in Sources */, + 2B46024728EDB1670008A624 /* CustomValueOptionSet.swift in Sources */, 35828CA0217A003F00ED546E /* OfflineDirections.swift in Sources */, 2B5F0E02273BEB3600CC2C1A /* RoadClassExclusionViolation.swift in Sources */, DAE7EA96230B5FD10003B211 /* Measurement.swift in Sources */, @@ -1614,6 +1634,7 @@ DA8F3A7423B56D3B00B56786 /* RouteLegTests.swift in Sources */, DA1A10F41D010251009F82FA /* V5Tests.swift in Sources */, DAE33A1D1F215DF600C06039 /* IntersectionTests.swift in Sources */, + 2B46025528EDB6300008A624 /* AttributeOptionsTests.swift in Sources */, 2B5407FE245B070A006C820B /* RouteRefreshTests.swift in Sources */, C5DAACB1201AA92B001F9261 /* MatchTests.swift in Sources */, DA1A10F51D010251009F82FA /* Fixture.swift in Sources */, @@ -1624,6 +1645,7 @@ DA4F84EF21C08BFB008A0434 /* WaypointTests.swift in Sources */, DAABF7902395ABA900CEEB61 /* SpokenInstructionTests.swift in Sources */, 35CC310D2285739700EA1966 /* WalkingOptionsTests.swift in Sources */, + 2B46025828EDB63B0008A624 /* CustomStringOptionSetTests.swift in Sources */, DABE6C80236A37E200D370F4 /* JSONSerialization.swift in Sources */, DAABF7942395AE9800CEEB61 /* GeoJSONTests.swift in Sources */, F4D785F11DDD82C100FF4665 /* RouteStepTests.swift in Sources */, @@ -1644,6 +1666,7 @@ C53A02281E92C271009837BD /* AttributeOptions.swift in Sources */, DA1A11061D0103A3009F82FA /* RouteLeg.swift in Sources */, C52552BC1FA15D6000B1545C /* VisualInstructionBanner.swift in Sources */, + 2B46024828EDB1670008A624 /* CustomValueOptionSet.swift in Sources */, 35828CA1217A003F00ED546E /* OfflineDirections.swift in Sources */, 2B5F0E03273BEB3600CC2C1A /* RoadClassExclusionViolation.swift in Sources */, DAE7EA97230B5FD10003B211 /* Measurement.swift in Sources */, @@ -1706,6 +1729,7 @@ DA2E03EB1CB0E13D00D1269A /* RouteOptions.swift in Sources */, C582BA2E2073ED6300647DAA /* Array.swift in Sources */, C52552B91FA15D5900B1545C /* VisualInstructionBanner.swift in Sources */, + 2B46024528EDB1670008A624 /* CustomValueOptionSet.swift in Sources */, 35828C9E217A003F00ED546E /* OfflineDirections.swift in Sources */, 2B5F0E00273BEB3600CC2C1A /* RoadClassExclusionViolation.swift in Sources */, DAE7EA94230B5FD10003B211 /* Measurement.swift in Sources */, @@ -1773,6 +1797,7 @@ DA6C9DAC1CAEC72800094FBC /* V5Tests.swift in Sources */, 8A3B4C9B24EB55F60085DA64 /* RouteResponseTests.swift in Sources */, DAE33A1B1F215DF600C06039 /* IntersectionTests.swift in Sources */, + 2B46025328EDB62E0008A624 /* AttributeOptionsTests.swift in Sources */, 2B5407FC245B070A006C820B /* RouteRefreshTests.swift in Sources */, C5DAACAF201AA92B001F9261 /* MatchTests.swift in Sources */, DA6C9DB21CAECA0E00094FBC /* Fixture.swift in Sources */, @@ -1783,6 +1808,7 @@ DA4F84ED21C08BFB008A0434 /* WaypointTests.swift in Sources */, DAABF78E2395ABA900CEEB61 /* SpokenInstructionTests.swift in Sources */, 35CC310B2285739700EA1966 /* WalkingOptionsTests.swift in Sources */, + 2B46025628EDB6390008A624 /* CustomStringOptionSetTests.swift in Sources */, DABE6C7E236A37E200D370F4 /* JSONSerialization.swift in Sources */, DAABF7922395AE9800CEEB61 /* GeoJSONTests.swift in Sources */, F4D785EF1DDD82C100FF4665 /* RouteStepTests.swift in Sources */, diff --git a/Sources/MapboxDirections/AttributeOptions.swift b/Sources/MapboxDirections/AttributeOptions.swift index f9096a29f..9e89a0b95 100644 --- a/Sources/MapboxDirections/AttributeOptions.swift +++ b/Sources/MapboxDirections/AttributeOptions.swift @@ -5,13 +5,19 @@ import Foundation When any of the attributes are specified, the resulting route leg contains one attribute value for each segment in leg, where a segment is the straight line between two coordinates in the route leg’s full geometry. */ -public struct AttributeOptions: OptionSet, CustomStringConvertible { +public struct AttributeOptions: CustomValueOptionSet, CustomStringConvertible { public var rawValue: Int + public var customOptionsByRawValue: [Int: String] = [:] + public init(rawValue: Int) { self.rawValue = rawValue } + public init() { + rawValue = 0 + } + /** Distance (in meters) along the segment. @@ -106,6 +112,11 @@ public struct AttributeOptions: OptionSet, CustomStringConvertible { if contains(.numericCongestionLevel) { descriptions.append("congestion_numeric") } + for (key, value) in customOptionsByRawValue { + if rawValue & key != 0 { + descriptions.append(value) + } + } return descriptions.joined(separator: ",") } } diff --git a/Sources/MapboxDirections/CustomValueOptionSet.swift b/Sources/MapboxDirections/CustomValueOptionSet.swift new file mode 100644 index 000000000..60066a90b --- /dev/null +++ b/Sources/MapboxDirections/CustomValueOptionSet.swift @@ -0,0 +1,635 @@ +import Foundation + +/// Describes how `customOptionsByRawValue` component is compared during logical operations in `CustomValueOptionSet`. +public enum CustomOptionComparisonPolicy { + /// Custom options are equal if `customOptions` key-value pairs are strictly equal + /// + /// Example: + /// [1: "value1"] == [1: "value1"] + /// [1: "value1"] != [1: "value2"] + /// [1: "value1"] != [:] + /// [:] == [:] + case equal + /// Custom options are equal if `customOptions` by the given key is equal or `nil` + /// + /// Example: + /// [1: "value1"] == [1: "value1"] + /// [1: "value1"] != [1: "value2"] + /// [1: "value1"] == [:] + /// [:] == [:] + case equalOrNull + /// Custom options are not compared. Only `rawValue` is taken into account when comparing `CustomStringOptionSet`s. + /// + /// Example: + /// [1: "value1"] == [1: "value1"] + /// [1: "value1"] == [1: "value2"] + /// [1: "value1"] == [:] + /// [:] == [:] + case rawValueEqual +} + +/// Option set implementation which allows each option to have custom string value attached. +public protocol CustomValueOptionSet: OptionSet where RawValue: FixedWidthInteger, Element == Self { + associatedtype Element = Self + associatedtype CustomValue: Equatable + var rawValue: Self.RawValue { get set } + + + /// Provides a text value description for user-provided options. + /// + /// The option set will recognize a custom option if it's unique `rawValue` flag is set and `customOptionsByRawValue` contains a description for that flag. + /// Use the `update(customOption:comparisonPolicy:)` method to append a custom option. + var customOptionsByRawValue: [RawValue: CustomValue] { get set } + + init(rawValue: Self.RawValue) + + /// Returns a Boolean value that indicates whether the given element exists + /// in the set. + /// + /// This example uses the `contains(_:)` method to test whether an integer is + /// a member of a set of prime numbers. + /// + /// let primes: Set = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37] + /// let x = 5 + /// if primes.contains(x) { + /// print("\(x) is prime!") + /// } else { + /// print("\(x). Not prime.") + /// } + /// // Prints "5 is prime!" + /// + /// - Parameter member: An element to look for in the set. + /// - Parameter comparisonPolicy: comparison method to be used for `customOptionsByRawValue`. + /// - Returns: `true` if `member` exists in the set; otherwise, `false`. + func contains(_ member: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) -> Bool + /// Returns a new set with the elements of both this and the given set. + /// + /// In the following example, the `attendeesAndVisitors` set is made up + /// of the elements of the `attendees` and `visitors` sets: + /// + /// let attendees: Set = ["Alicia", "Bethany", "Diana"] + /// let visitors = ["Marcia", "Nathaniel"] + /// let attendeesAndVisitors = attendees.union(visitors) + /// print(attendeesAndVisitors) + /// // Prints "["Diana", "Nathaniel", "Bethany", "Alicia", "Marcia"]" + /// + /// If the set already contains one or more elements that are also in + /// `other`, the existing members are kept. + /// + /// let initialIndices = Set(0..<5) + /// let expandedIndices = initialIndices.union([2, 3, 6, 7]) + /// print(expandedIndices) + /// // Prints "[2, 4, 6, 7, 0, 1, 3]" + /// + /// - Parameter other: A set of the same type as the current set. + /// - Parameter comparisonPolicy: comparison method to be used for `customOptionsByRawValue`. + /// - Returns: A new set with the unique elements of this set and `other`. + /// + /// - Note: if this set and `other` contain elements that are equal but + /// distinguishable (e.g. via `===`), which of these elements is present + /// in the result is unspecified. + func union(_ other: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) -> Self.Element + /// Adds the elements of the given set to the set. + /// + /// In the following example, the elements of the `visitors` set are added to + /// the `attendees` set: + /// + /// var attendees: Set = ["Alicia", "Bethany", "Diana"] + /// let visitors: Set = ["Diana", "Marcia", "Nathaniel"] + /// attendees.formUnion(visitors) + /// print(attendees) + /// // Prints "["Diana", "Nathaniel", "Bethany", "Alicia", "Marcia"]" + /// + /// If the set already contains one or more elements that are also in + /// `other`, the existing members are kept. + /// + /// var initialIndices = Set(0..<5) + /// initialIndices.formUnion([2, 3, 6, 7]) + /// print(initialIndices) + /// // Prints "[2, 4, 6, 7, 0, 1, 3]" + /// + /// - Parameter other: A set of the same type as the current set. + /// - Parameter comparisonPolicy: comparison method to be used for `customOptionsByRawValue`. + mutating func formUnion(_ other: Self, comparisonPolicy: CustomOptionComparisonPolicy) + /// Returns a new set with the elements that are common to both this set and + /// the given set. + /// + /// In the following example, the `bothNeighborsAndEmployees` set is made up + /// of the elements that are in *both* the `employees` and `neighbors` sets. + /// Elements that are in only one or the other are left out of the result of + /// the intersection. + /// + /// let employees: Set = ["Alicia", "Bethany", "Chris", "Diana", "Eric"] + /// let neighbors: Set = ["Bethany", "Eric", "Forlani", "Greta"] + /// let bothNeighborsAndEmployees = employees.intersection(neighbors) + /// print(bothNeighborsAndEmployees) + /// // Prints "["Bethany", "Eric"]" + /// + /// - Parameter other: A set of the same type as the current set. + /// - Parameter comparisonPolicy: comparison method to be used for `customOptionsByRawValue`. + /// - Returns: A new set. + /// + /// - Note: if this set and `other` contain elements that are equal but + /// distinguishable (e.g. via `===`), which of these elements is present + /// in the result is unspecified. + func intersection(_ other: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) -> Self.Element + /// Removes the elements of this set that aren't also in the given set. + /// + /// In the following example, the elements of the `employees` set that are + /// not also members of the `neighbors` set are removed. In particular, the + /// names `"Alicia"`, `"Chris"`, and `"Diana"` are removed. + /// + /// var employees: Set = ["Alicia", "Bethany", "Chris", "Diana", "Eric"] + /// let neighbors: Set = ["Bethany", "Eric", "Forlani", "Greta"] + /// employees.formIntersection(neighbors) + /// print(employees) + /// // Prints "["Bethany", "Eric"]" + /// + /// - Parameter other: A set of the same type as the current set. + /// - Parameter comparisonPolicy: comparison method to be used for `customOptionsByRawValue`. + mutating func formIntersection(_ other: Self, comparisonPolicy: CustomOptionComparisonPolicy) + /// Returns a new set with the elements that are either in this set or in the + /// given set, but not in both. + /// + /// In the following example, the `eitherNeighborsOrEmployees` set is made up + /// of the elements of the `employees` and `neighbors` sets that are not in + /// both `employees` *and* `neighbors`. In particular, the names `"Bethany"` + /// and `"Eric"` do not appear in `eitherNeighborsOrEmployees`. + /// + /// let employees: Set = ["Alicia", "Bethany", "Diana", "Eric"] + /// let neighbors: Set = ["Bethany", "Eric", "Forlani"] + /// let eitherNeighborsOrEmployees = employees.symmetricDifference(neighbors) + /// print(eitherNeighborsOrEmployees) + /// // Prints "["Diana", "Forlani", "Alicia"]" + /// + /// - Parameter other: A set of the same type as the current set. + /// - Parameter comparisonPolicy: comparison method to be used for `customOptionsByRawValue`. + /// - Returns: A new set. + func symmetricDifference(_ other: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) -> Self.Element + /// Removes the elements of the set that are also in the given set and adds + /// the members of the given set that are not already in the set. + /// + /// In the following example, the elements of the `employees` set that are + /// also members of `neighbors` are removed from `employees`, while the + /// elements of `neighbors` that are not members of `employees` are added to + /// `employees`. In particular, the names `"Bethany"` and `"Eric"` are + /// removed from `employees` while the name `"Forlani"` is added. + /// + /// var employees: Set = ["Alicia", "Bethany", "Diana", "Eric"] + /// let neighbors: Set = ["Bethany", "Eric", "Forlani"] + /// employees.formSymmetricDifference(neighbors) + /// print(employees) + /// // Prints "["Diana", "Forlani", "Alicia"]" + /// + /// - Parameter other: A set of the same type. + /// - Parameter comparisonPolicy: comparison method to be used for `customOptionsByRawValue`. + mutating func formSymmetricDifference(_ other: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) + /// Returns a new set containing the elements of this set that do not occur + /// in the given set. + /// + /// In the following example, the `nonNeighbors` set is made up of the + /// elements of the `employees` set that are not elements of `neighbors`: + /// + /// let employees: Set = ["Alicia", "Bethany", "Chris", "Diana", "Eric"] + /// let neighbors: Set = ["Bethany", "Eric", "Forlani", "Greta"] + /// let nonNeighbors = employees.subtracting(neighbors) + /// print(nonNeighbors) + /// // Prints "["Diana", "Chris", "Alicia"]" + /// + /// - Parameter other: A set of the same type as the current set. + /// - Parameter comparisonPolicy: comparison method to be used for `customOptionsByRawValue`. + /// - Returns: A new set. + func subtracting(_ other: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) -> Self.Element + /// Removes the elements of the given set from this set. + /// + /// In the following example, the elements of the `employees` set that are + /// also members of the `neighbors` set are removed. In particular, the + /// names `"Bethany"` and `"Eric"` are removed from `employees`. + /// + /// var employees: Set = ["Alicia", "Bethany", "Chris", "Diana", "Eric"] + /// let neighbors: Set = ["Bethany", "Eric", "Forlani", "Greta"] + /// employees.subtract(neighbors) + /// print(employees) + /// // Prints "["Diana", "Chris", "Alicia"]" + /// + /// - Parameter other: A set of the same type as the current set. + /// - Parameter comparisonPolicy: comparison method to be used for `customOptionsByRawValue`. + mutating func subtract(_ other: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) + /// Inserts the given element in the set if it is not already present. + /// + /// If an element equal to `newMember` is already contained in the set, this + /// method has no effect. In this example, a new element is inserted into + /// `classDays`, a set of days of the week. When an existing element is + /// inserted, the `classDays` set does not change. + /// + /// enum DayOfTheWeek: Int { + /// case sunday, monday, tuesday, wednesday, thursday, + /// friday, saturday + /// } + /// + /// var classDays: Set = [.wednesday, .friday] + /// print(classDays.insert(.monday)) + /// // Prints "(true, .monday)" + /// print(classDays) + /// // Prints "[.friday, .wednesday, .monday]" + /// + /// print(classDays.insert(.friday)) + /// // Prints "(false, .friday)" + /// print(classDays) + /// // Prints "[.friday, .wednesday, .monday]" + /// + /// - Parameter newMember: An element to insert into the set. + /// - Parameter comparisonPolicy: comparison method to be used for `customOptionsByRawValue`. + /// - Returns: `(true, newMember)` if `newMember` was not contained in the + /// set. If an element equal to `newMember` was already contained in the + /// set, the method returns `(false, oldMember)`, where `oldMember` is the + /// element that was equal to `newMember`. In some cases, `oldMember` may + /// be distinguishable from `newMember` by identity comparison or some + /// other means. + mutating func insert(_ newMember: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) -> (inserted: Bool, memberAfterInsert: Self.Element) + /// Removes the given element and any elements subsumed by the given element. + /// + /// - Parameter member: The element of the set to remove. + /// - Parameter comparisonPolicy: comparison method to be used for `customOptionsByRawValue`. + /// - Returns: For ordinary sets, an element equal to `member` if `member` is + /// contained in the set; otherwise, `nil`. In some cases, a returned + /// element may be distinguishable from `member` by identity comparison + /// or some other means. + /// + /// For sets where the set type and element type are the same, like + /// `OptionSet` types, this method returns any intersection between the set + /// and `[member]`, or `nil` if the intersection is empty. + mutating func remove(_ member: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) -> Self.Element? + /// Inserts the given element into the set unconditionally. + /// + /// If an element equal to `newMember` is already contained in the set, + /// `newMember` replaces the existing element. In this example, an existing + /// element is inserted into `classDays`, a set of days of the week. + /// + /// enum DayOfTheWeek: Int { + /// case sunday, monday, tuesday, wednesday, thursday, + /// friday, saturday + /// } + /// + /// var classDays: Set = [.monday, .wednesday, .friday] + /// print(classDays.update(with: .monday)) + /// // Prints "Optional(.monday)" + /// + /// - Parameter newMember: An element to insert into the set. + /// - Parameter comparisonPolicy: comparison method to be used for `customOptionsByRawValue`. + /// - Returns: For ordinary sets, an element equal to `newMember` if the set + /// already contained such a member; otherwise, `nil`. In some cases, the + /// returned element may be distinguishable from `newMember` by identity + /// comparison or some other means. + /// + /// For sets where the set type and element type are the same, like + /// `OptionSet` types, this method returns any intersection between the + /// set and `[newMember]`, or `nil` if the intersection is empty. + mutating func update(with newMember: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) -> Self.Element? + /// Inserts the given element into the set unconditionally. + /// + /// If an element equal to `customOption` is already contained in the set, + /// `customOption` replaces the existing element. Otherwise - updates the set contents and fills `customOptionsByRawValue` accordingly. + /// + /// - Parameter customOption: An element to insert into the set. + /// - Parameter comparisonPolicy: comparison method to be used for `customOptionsByRawValue`. + /// - Returns: For ordinary sets, an element equal to `customOption` if the set + /// already contained such a member; otherwise, `nil`. In some cases, the + /// returned element may be distinguishable from `customOption` by identity + /// comparison or some other means. + /// + /// For sets where the set type and element type are the same, like + /// `OptionSet` types, this method returns any intersection between the + /// set and `[customOption]`, or `nil` if the intersection is empty. + mutating func update(customOption: (RawValue, CustomValue), comparisonPolicy: CustomOptionComparisonPolicy) -> Self.Element? + /// Returns a Boolean value that indicates whether the set is a subset of + /// another set. + /// + /// Set *A* is a subset of another set *B* if every member of *A* is also a + /// member of *B*. + /// + /// let employees: Set = ["Alicia", "Bethany", "Chris", "Diana", "Eric"] + /// let attendees: Set = ["Alicia", "Bethany", "Diana"] + /// print(attendees.isSubset(of: employees)) + /// // Prints "true" + /// + /// - Parameter other: A set of the same type as the current set. + /// - Parameter comparisonPolicy: comparison method to be used for `customOptionsByRawValue`. + /// - Returns: `true` if the set is a subset of `other`; otherwise, `false`. + func isSubset(of other: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) -> Bool + /// Returns a Boolean value that indicates whether the set is a superset of + /// the given set. + /// + /// Set *A* is a superset of another set *B* if every member of *B* is also a + /// member of *A*. + /// + /// let employees: Set = ["Alicia", "Bethany", "Chris", "Diana", "Eric"] + /// let attendees: Set = ["Alicia", "Bethany", "Diana"] + /// print(employees.isSuperset(of: attendees)) + /// // Prints "true" + /// + /// - Parameter other: A set of the same type as the current set. + /// - Parameter comparisonPolicy: comparison method to be used for `customOptionsByRawValue`. + /// - Returns: `true` if the set is a superset of `possibleSubset`; + /// otherwise, `false`. + func isSuperset(of other: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) -> Bool + /// Returns a Boolean value that indicates whether this set is a strict + /// subset of the given set. + /// + /// Set *A* is a strict subset of another set *B* if every member of *A* is + /// also a member of *B* and *B* contains at least one element that is not a + /// member of *A*. + /// + /// let employees: Set = ["Alicia", "Bethany", "Chris", "Diana", "Eric"] + /// let attendees: Set = ["Alicia", "Bethany", "Diana"] + /// print(attendees.isStrictSubset(of: employees)) + /// // Prints "true" + /// + /// // A set is never a strict subset of itself: + /// print(attendees.isStrictSubset(of: attendees)) + /// // Prints "false" + /// + /// - Parameter other: A set of the same type as the current set. + /// - Parameter comparisonPolicy: comparison method to be used for `customOptionsByRawValue`. + /// - Returns: `true` if the set is a strict subset of `other`; otherwise, + /// `false`. + func isStrictSubset(of other: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) -> Bool + /// Returns a Boolean value that indicates whether this set is a strict + /// superset of the given set. + /// + /// Set *A* is a strict superset of another set *B* if every member of *B* is + /// also a member of *A* and *A* contains at least one element that is *not* + /// a member of *B*. + /// + /// let employees: Set = ["Alicia", "Bethany", "Chris", "Diana", "Eric"] + /// let attendees: Set = ["Alicia", "Bethany", "Diana"] + /// print(employees.isStrictSuperset(of: attendees)) + /// // Prints "true" + /// + /// // A set is never a strict superset of itself: + /// print(employees.isStrictSuperset(of: employees)) + /// // Prints "false" + /// + /// - Parameter other: A set of the same type as the current set. + /// - Parameter comparisonPolicy: comparison method to be used for `customOptionsByRawValue`. + /// - Returns: `true` if the set is a strict superset of `other`; otherwise, + /// `false`. + func isStrictSuperset(of other: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) -> Bool + /// Returns a Boolean value that indicates whether the set has no members in + /// common with the given set. + /// + /// In the following example, the `employees` set is disjoint with the + /// `visitors` set because no name appears in both sets. + /// + /// let employees: Set = ["Alicia", "Bethany", "Chris", "Diana", "Eric"] + /// let visitors: Set = ["Marcia", "Nathaniel", "Olivia"] + /// print(employees.isDisjoint(with: visitors)) + /// // Prints "true" + /// + /// - Parameter other: A set of the same type as the current set. + /// - Parameter comparisonPolicy: comparison method to be used for `customOptionsByRawValue`. + /// - Returns: `true` if the set has no elements in common with `other`; + /// otherwise, `false`. + func isDisjoint(with other: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) -> Bool +} + +public extension CustomValueOptionSet where Self == Self.Element { + + // MARK: Implemented methods + + private func customOptionIsEqual(_ lhs: [RawValue: CustomValue], + _ rhs: [RawValue: CustomValue], + key: RawValue, + policy: CustomOptionComparisonPolicy) -> Bool { + switch policy { + case .equal: + return lhs[key] == rhs[key] + case .equalOrNull: + return lhs[key] == rhs[key] || lhs[key] == nil || rhs[key] == nil + case .rawValueEqual: + return true + } + } + + @discardableResult + func contains(_ member: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) -> Bool { + let intersection = rawValue & member.rawValue + guard intersection != 0 else { + return false + } + + for offset in 0.. (inserted: Bool, memberAfterInsert: Self.Element) { + + if contains(newMember, comparisonPolicy: comparisonPolicy) { + return (false, intersection(newMember, comparisonPolicy: comparisonPolicy)) + } else { + rawValue = rawValue | newMember.rawValue + customOptionsByRawValue.merge(newMember.customOptionsByRawValue) { current, _ in current } + return (true, newMember) + } + } + + @discardableResult @inlinable + mutating func remove(_ member: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) -> Self.Element? { + + let intersection = intersection(member, comparisonPolicy: comparisonPolicy) + if intersection.rawValue == 0 { + return nil + } else { + rawValue -= intersection.rawValue + customOptionsByRawValue = customOptionsByRawValue.filter { (key, _) in + rawValue & key != 0 + } + return intersection + } + } + + @discardableResult @inlinable + mutating func update(with newMember: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) -> Self.Element? { + let intersection = intersection(newMember, comparisonPolicy: comparisonPolicy) + + if intersection.rawValue == 0 { + // insert + rawValue = rawValue | newMember.rawValue + customOptionsByRawValue.merge(newMember.customOptionsByRawValue) { current, _ in current } + return nil + } else { + // update + rawValue = rawValue | newMember.rawValue + customOptionsByRawValue.merge(intersection.customOptionsByRawValue) { _, new in new } + return intersection + } + } + + mutating func formIntersection(_ other: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) { + rawValue = rawValue & other.rawValue + customOptionsByRawValue = customOptionsByRawValue.reduce(into: [:]) { (partialResult, item) in + if customOptionIsEqual(customOptionsByRawValue, other.customOptionsByRawValue, key: item.key, policy: comparisonPolicy) { + partialResult[item.key] = item.value + } else if rawValue & item.key != 0 { + rawValue -= item.key + } + } + } + + mutating func subtract(_ other: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) { + rawValue = rawValue ^ (rawValue & other.rawValue) + customOptionsByRawValue = customOptionsByRawValue.reduce(into: [:], { partialResult, item in + if !customOptionIsEqual(customOptionsByRawValue, other.customOptionsByRawValue, key: item.key, policy: comparisonPolicy) { + partialResult[item.key] = item.value + } + }) + } + + // MARK: Deferring methods + + @discardableResult @inlinable + func union(_ other: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) -> Self.Element { + var union = self + union.formUnion(other, comparisonPolicy: comparisonPolicy) + return union + } + + @discardableResult @inlinable + func intersection(_ other: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) -> Self.Element { + var intersection = self + intersection.formIntersection(other, comparisonPolicy: comparisonPolicy) + return intersection + } + + @discardableResult @inlinable + func symmetricDifference(_ other: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) -> Self.Element { + var difference = self + difference.formSymmetricDifference(other, comparisonPolicy: comparisonPolicy) + return difference + } + + @discardableResult @inlinable + mutating func update(customOption: (RawValue, CustomValue), comparisonPolicy: CustomOptionComparisonPolicy) -> Self.Element? { + var newMember = Self(rawValue: customOption.0) + newMember.customOptionsByRawValue[customOption.0] = customOption.1 + return update(with: newMember, comparisonPolicy: comparisonPolicy) + } + + @inlinable + mutating func formUnion(_ other: Self, comparisonPolicy: CustomOptionComparisonPolicy) { + _ = update(with: other, comparisonPolicy: comparisonPolicy) + } + + @inlinable + mutating func formSymmetricDifference(_ other: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) { + let intersection = intersection(other, comparisonPolicy: comparisonPolicy) + _ = remove(other, comparisonPolicy: comparisonPolicy) + _ = insert(other.subtracting(intersection, + comparisonPolicy: comparisonPolicy), + comparisonPolicy: comparisonPolicy) + } + + @discardableResult @inlinable + func subtracting(_ other: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) -> Self.Element { + var substracted = self + substracted.subtract(other, comparisonPolicy: comparisonPolicy) + return substracted + } + + @discardableResult @inlinable + func isSubset(of other: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) -> Bool { + return intersection(other, comparisonPolicy: comparisonPolicy) == self + } + + @discardableResult @inlinable + func isDisjoint(with other: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) -> Bool { + return intersection(other, comparisonPolicy: comparisonPolicy).isEmpty + } + + @discardableResult @inlinable + func isSuperset(of other: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) -> Bool { + return other.isSubset(of: self, comparisonPolicy: comparisonPolicy) + } + + @discardableResult @inlinable + func isStrictSuperset(of other: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) -> Bool { + return isSuperset(of: other, comparisonPolicy: comparisonPolicy) && rawValue > other.rawValue + } + + @discardableResult @inlinable + func isStrictSubset(of other: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) -> Bool { + return other.isStrictSuperset(of: self, comparisonPolicy: comparisonPolicy) + } +} + +// MARK: - SetAlgebra implementation +public extension CustomValueOptionSet { + @discardableResult @inlinable + func contains(_ member: Self.Element) -> Bool { + return contains(member, comparisonPolicy: .equal) + } + @discardableResult @inlinable + func union(_ other: Self) -> Self { + return union(other, comparisonPolicy: .equal) + } + @discardableResult @inlinable + func intersection(_ other: Self) -> Self { + return intersection(other, comparisonPolicy: .equal) + } + @discardableResult @inlinable + func symmetricDifference(_ other: Self) -> Self { + return symmetricDifference(other, comparisonPolicy: .equal) + } + @discardableResult @inlinable + mutating func insert(_ newMember: Self.Element) -> (inserted: Bool, memberAfterInsert: Self.Element) { + return insert(newMember, comparisonPolicy: .equal) + } + @discardableResult @inlinable + mutating func remove(_ member: Self.Element) -> Self.Element? { + return remove(member, comparisonPolicy: .equal) + } + @discardableResult @inlinable + mutating func update(with newMember: Self.Element) -> Self.Element? { + return update(with: newMember, comparisonPolicy: .equal) + } + @inlinable + mutating func formUnion(_ other: Self) { + formUnion(other, comparisonPolicy: .equal) + } + @inlinable + mutating func formIntersection(_ other: Self) { + formIntersection(other, comparisonPolicy: .equal) + } + @inlinable + mutating func formSymmetricDifference(_ other: Self) { + formSymmetricDifference(other, comparisonPolicy: .equal) + } + @discardableResult @inlinable + func subtracting(_ other: Self) -> Self { + return subtracting(other, comparisonPolicy: .equal) + } + @discardableResult @inlinable + func isSubset(of other: Self) -> Bool { + return isSubset(of: other, comparisonPolicy: .equal) + } + @discardableResult @inlinable + func isDisjoint(with other: Self) -> Bool { + return isDisjoint(with: other, comparisonPolicy: .equal) + } + @discardableResult @inlinable + func isSuperset(of other: Self) -> Bool { + return isSuperset(of: other, comparisonPolicy: .equal) + } + @inlinable + mutating func subtract(_ other: Self) { + subtract(other, comparisonPolicy: .equal) + } +} diff --git a/Tests/MapboxDirectionsTests/AttributeOptionsTests.swift b/Tests/MapboxDirectionsTests/AttributeOptionsTests.swift new file mode 100644 index 000000000..6813459ee --- /dev/null +++ b/Tests/MapboxDirectionsTests/AttributeOptionsTests.swift @@ -0,0 +1,119 @@ +import XCTest +import MapboxDirections + +class AttributeOptionsTests: XCTestCase { + func testInsertion() { + var options = AttributeOptions() + var options2merge = AttributeOptions(descriptions: ["speed"])! + var optionsWithCustom = AttributeOptions() + + optionsWithCustom.update(customOption: (1<<7, "Custom7"), comparisonPolicy: .equal) + options.update(with: .distance) + options.update(with: optionsWithCustom) + options2merge.update(customOption: (1<<8, "Custom_8"), comparisonPolicy: .equal) + + options.update(with: options2merge) + + // Check merged options are collected + XCTAssertEqual(options.rawValue, + AttributeOptions.speed.rawValue + AttributeOptions.distance.rawValue + 1<<7 + 1<<8) + XCTAssertEqual(options.description.split(separator: ",").count, + 4) + XCTAssertEqual(optionsWithCustom, + options.update(customOption: (1<<7, "Custom7"), comparisonPolicy: .equal)) + + // insert existing default + XCTAssertFalse(options.insert(.distance).inserted) + // insert existing custom + XCTAssertFalse(options.insert(optionsWithCustom).inserted) + // insert conflicting custom + var optionsWithConflict = AttributeOptions() + optionsWithConflict.update(customOption: (optionsWithCustom.rawValue, "Another custom name"), comparisonPolicy: .equal) + XCTAssertFalse(options.insert(optionsWithConflict, comparisonPolicy: .rawValueEqual).inserted) + // insert custom with default raw + optionsWithConflict.rawValue = AttributeOptions.distance.rawValue + XCTAssertFalse(options.insert(optionsWithConflict).inserted) + } + + func testContains() { + var options = AttributeOptions() + options.update(with: .expectedTravelTime) + options.update(customOption: (1<<9, "Custom"), comparisonPolicy: .equal) + + XCTAssertTrue(options.contains(.init(rawValue: AttributeOptions.expectedTravelTime.rawValue))) + XCTAssertFalse(options.contains(.congestionLevel)) + + var wrongCustomOption = AttributeOptions() + wrongCustomOption.update(customOption: (1<<9, "Wrong name"), comparisonPolicy: .equal) + XCTAssertFalse(options.contains(wrongCustomOption)) + + var correctCustomOption = AttributeOptions() + correctCustomOption.update(customOption: (1<<9, "Custom"), comparisonPolicy: .equal) + XCTAssertTrue(options.contains(correctCustomOption)) + + XCTAssertTrue(options.contains(.init(rawValue: 1<<9), comparisonPolicy: .equalOrNull)) + } + + func testRemove() { + var preservedOption = AttributeOptions() + preservedOption.update(customOption: (1<<12, "Should be preserved"), comparisonPolicy: .equal) + var options = AttributeOptions() + options.update(with: .congestionLevel) + options.update(with: .distance) + options.update(customOption: (1<<10, "Custom"), comparisonPolicy: .equal) + options.update(with: preservedOption) + + // Removing default item + let distance = options.remove(AttributeOptions(descriptions: ["distance"])!) + + XCTAssertEqual(distance?.rawValue, AttributeOptions.distance.rawValue) + XCTAssertTrue(options.contains(.congestionLevel)) + XCTAssertTrue(options.contains(preservedOption)) + + // Removing not existing item by raw value + XCTAssertNil(options.remove(AttributeOptions(rawValue: 1))) + XCTAssertTrue(options.contains(.congestionLevel)) + XCTAssertTrue(options.contains(preservedOption)) + + // Removing custom option with incorrect name + var wrongCustomOption = AttributeOptions() + wrongCustomOption.update(customOption: (1<<10, "Wrong name"), comparisonPolicy: .equal) + + XCTAssertNil(options.remove(wrongCustomOption)) + XCTAssertTrue(options.contains(.congestionLevel)) + XCTAssertTrue(options.contains(preservedOption)) + + // Removing existing custom option + var correctCustomOption = AttributeOptions() + correctCustomOption.update(customOption: (1<<10, "Custom"), comparisonPolicy: .equal) + + XCTAssertEqual(options.remove(correctCustomOption), correctCustomOption) + XCTAssertTrue(options.contains(.congestionLevel)) + XCTAssertTrue(options.contains(preservedOption)) + + // Removing custom option with default raw value + var customOptionWithDefaultRaw = AttributeOptions() + customOptionWithDefaultRaw.update(customOption: (AttributeOptions.distance.rawValue, "Not a distance"), comparisonPolicy: .equal) + XCTAssertNil(options.remove(customOptionWithDefaultRaw)) + + // Removing custom option by raw value only + options.update(with: correctCustomOption) + XCTAssertEqual(options.remove(.init(rawValue: 1<<10), comparisonPolicy: .equalOrNull), correctCustomOption) + } + + func testCustomAttributes() { + let customOption1 = (1, "atmospheric pressure") + let customOption2 = (1<<10, "space radiation") + var attributes = AttributeOptions() + attributes.insert(.congestionLevel) + attributes.insert(.speed) + attributes.update(customOption: customOption1, comparisonPolicy: .equal) + attributes.update(customOption: customOption2, comparisonPolicy: .equal) + + let descriptions = attributes.description.split(separator: ",") + XCTAssertTrue(descriptions.contains { $0 == AttributeOptions.congestionLevel.description }) + XCTAssertTrue(descriptions.contains { $0 == AttributeOptions.speed.description }) + XCTAssertTrue(descriptions.contains { $0 == customOption1.1 }) + XCTAssertTrue(descriptions.contains { $0 == customOption2.1 }) + } +} diff --git a/Tests/MapboxDirectionsTests/CustomStringOptionSetTests.swift b/Tests/MapboxDirectionsTests/CustomStringOptionSetTests.swift new file mode 100644 index 000000000..6ffcf089d --- /dev/null +++ b/Tests/MapboxDirectionsTests/CustomStringOptionSetTests.swift @@ -0,0 +1,116 @@ +import XCTest +import MapboxDirections + +struct BareCustomStringOptionSet : CustomValueOptionSet { + init() { + rawValue = 0 + } + + init(rawValue: Int) { + self.rawValue = rawValue + } + + var rawValue: Int + var customOptionsByRawValue: [Int: String] = [:] + var description: String = "" +} + +/// Tests conformance to SetAlgebra [conditions](https://developer.apple.com/documentation/swift/setalgebra#Conforming-to-the-SetAlgebra-Protocol). +class CustomStringOptionSetTests: XCTestCase { + func getEmptySet() -> BareCustomStringOptionSet { + var set = BareCustomStringOptionSet() + set.customOptionsByRawValue = [1: "value"] + return set + } + + func getSet1() -> BareCustomStringOptionSet { + var set = BareCustomStringOptionSet(rawValue: 1+2+4+8) + set.customOptionsByRawValue = [2: "value 2", + 8: "value 8"] + return set + } + + func getSet2() -> BareCustomStringOptionSet { + var set = BareCustomStringOptionSet(rawValue: 4+8+16+32) + set.customOptionsByRawValue = [8: "value 8", + 32: "value 32"] + return set + } + + func getSubset() -> BareCustomStringOptionSet { + var set = BareCustomStringOptionSet(rawValue: 4+8) + set.customOptionsByRawValue = [8: "value 8"] + return set + } + + // S() == [] + func testEmptySet() { + let set = getEmptySet() + + XCTAssertTrue(set.isEmpty) + } + + // x.intersection(x) == x + // x.intersection([]) == [] + func testIntersection() { + let emptySet = getEmptySet() + let set = getSet1() + + XCTAssertEqual(set, set.intersection(set, + comparisonPolicy: .equal)) + XCTAssertEqual(emptySet, set.intersection(emptySet, + comparisonPolicy: .equal)) + } + + // x.union(x) == x + // x.union([]) == x + func testUnion() { + let emptySet = getEmptySet() + let set = getSet1() + + XCTAssertEqual(set, set.union(set, + comparisonPolicy: .equal)) + XCTAssertEqual(set, set.union(emptySet, + comparisonPolicy: .equal)) + } + + // x.contains(e) implies x.union(y).contains(e) + // x.union(y).contains(e) implies x.contains(e) || y.contains(e) + // x.contains(e) && y.contains(e) if and only if x.intersection(y).contains(e) + func testContains() { + let set1 = getSet1() + let set2 = getSet2() + let setE = getSubset() + + XCTAssertTrue(set1.contains(setE, comparisonPolicy: .equal)) + XCTAssertTrue(set1.union(set2, comparisonPolicy: .equal).contains(setE, comparisonPolicy: .equal)) + + XCTAssertTrue(set1.intersection(set2, comparisonPolicy: .equal).contains(setE, comparisonPolicy: .equal)) + } + + // x.isSubset(of: y) implies x.union(y) == y + // x.isSuperset(of: y) implies x.union(y) == x + // x.isSubset(of: y) if and only if y.isSuperset(of: x) + func testSuperset() { + let set1 = getSet1() + let setE = getSubset() + + XCTAssertTrue(setE.isSubset(of: set1, + comparisonPolicy: .equal)) + XCTAssertEqual(setE.union(set1, comparisonPolicy: .equal), set1) + XCTAssertEqual(set1.union(setE, comparisonPolicy: .equal), set1) + XCTAssertTrue(set1.isSuperset(of: setE, + comparisonPolicy: .equal)) + } + + // x.isStrictSuperset(of: y) if and only if x.isSuperset(of: y) && x != y + // x.isStrictSubset(of: y) if and only if x.isSubset(of: y) && x != y + func testStrictSuperset() { + let set1 = getSet1() + let setE = getSubset() + + XCTAssertTrue(set1.isStrictSuperset(of: setE, comparisonPolicy: .equal)) + XCTAssertTrue(setE.isStrictSubset(of: set1, comparisonPolicy: .equal)) + XCTAssertNotEqual(set1, setE) + } +}