From b8dba5930f293277d40ea4fb7b09af76a688ce44 Mon Sep 17 00:00:00 2001
From: Jacob Christie <19879272+jjatie@users.noreply.github.com>
Date: Sun, 31 Jan 2021 21:52:41 -0400
Subject: [PATCH] Import swift algorithms (#4497)

* Use Algorithms implementation of binary search

* Use `indexed()` where appropriate

* Update Package.swift to include SwiftAlgorithms

* Use lazy implementation of `prefix(while:)`

For better performance

* Revert "Use lazy implementation of `prefix(while:)`"

This reverts commit 69a303f2178c51d95fa9f1e677d28a95c70b148d.
---
 Charts.xcodeproj/project.pbxproj              |  77 ++++++--
 .../xcshareddata/swiftpm/Package.resolved     |  25 +++
 Package.resolved                              |  25 +++
 Package.swift                                 |   8 +-
 .../Standard/ChartDataSet.swift               | 176 +++++-------------
 .../Charts/Highlight/ChartHighlighter.swift   |   3 +-
 .../Highlight/CombinedHighlighter.swift       |   7 +-
 .../Charts/Highlight/RadarHighlighter.swift   |   2 +-
 .../Renderers/YAxisRendererRadarChart.swift   |   2 +-
 9 files changed, 174 insertions(+), 151 deletions(-)
 create mode 100644 Charts.xcworkspace/xcshareddata/swiftpm/Package.resolved
 create mode 100644 Package.resolved

diff --git a/Charts.xcodeproj/project.pbxproj b/Charts.xcodeproj/project.pbxproj
index 8f9b5913b1..e062be39b4 100644
--- a/Charts.xcodeproj/project.pbxproj
+++ b/Charts.xcodeproj/project.pbxproj
@@ -3,7 +3,7 @@
 	archiveVersion = 1;
 	classes = {
 	};
-	objectVersion = 46;
+	objectVersion = 52;
 	objects = {
 
 /* Begin PBXBuildFile section */
@@ -29,6 +29,7 @@
 		219192CA6B4895319AB49DCA /* BarLineScatterCandleBubbleRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B1C588E9DF6FFD56D7ADF8E /* BarLineScatterCandleBubbleRenderer.swift */; };
 		221CA2922588FCBC00C2DD1E /* Sequence+KeyPath.swift in Sources */ = {isa = PBXBuildFile; fileRef = 221CA2912588FCBC00C2DD1E /* Sequence+KeyPath.swift */; };
 		2243BBFD1FF156EC00B49D0B /* EquatableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2243BBFB1FF156D000B49D0B /* EquatableTests.swift */; };
+		228D56232554F27A00BEE75E /* Algorithms in Frameworks */ = {isa = PBXBuildFile; productRef = 228D56222554F27A00BEE75E /* Algorithms */; };
 		23649EFC635A76022F07FFA6 /* PieChartDataEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD02157CF8CEE1189BF681DA /* PieChartDataEntry.swift */; };
 		23FA50B2730D8C7ACA091C4F /* BarChartRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75F279974FE650E57A061B09 /* BarChartRenderer.swift */; };
 		24151B0729D77251A8494D70 /* LineRadarRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 105FFC9D3773A9C7A60A897F /* LineRadarRenderer.swift */; };
@@ -344,6 +345,7 @@
 			isa = PBXFrameworksBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
+				228D56232554F27A00BEE75E /* Algorithms in Frameworks */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -748,6 +750,9 @@
 			dependencies = (
 			);
 			name = Charts;
+			packageProductDependencies = (
+				228D56222554F27A00BEE75E /* Algorithms */,
+			);
 			productName = Charts;
 			productReference = 65AD9E95D9ED4DC0BD73A743 /* Charts.framework */;
 			productType = "com.apple.product-type.framework";
@@ -798,6 +803,9 @@
 				Base,
 			);
 			mainGroup = 865A1CF149F52850CAB7F177;
+			packageReferences = (
+				228D56212554F27A00BEE75E /* XCRemoteSwiftPackageReference "swift-algorithms" */,
+			);
 			productRefGroup = AB2D554102718F209377399E /* Products */;
 			projectDirPath = "";
 			projectRoot = "";
@@ -1050,7 +1058,11 @@
 				INFOPLIST_FILE = "Source/Supporting Files/Info.plist";
 				INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
 				IPHONEOS_DEPLOYMENT_TARGET = 9.0;
-				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks";
+				LD_RUNPATH_SEARCH_PATHS = (
+					"$(inherited)",
+					"@executable_path/../Frameworks",
+					"@loader_path/Frameworks",
+				);
 				MACOSX_DEPLOYMENT_TARGET = 10.11;
 				MARKETING_VERSION = 4.0.0;
 				MTL_ENABLE_DEBUG_INFO = YES;
@@ -1085,8 +1097,16 @@
 				GCC_NO_COMMON_BLOCKS = YES;
 				INFOPLIST_FILE = "Tests/Supporting Files/Info.plist";
 				IPHONEOS_DEPLOYMENT_TARGET = 9.0;
-				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
-				"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks";
+				LD_RUNPATH_SEARCH_PATHS = (
+					"$(inherited)",
+					"@executable_path/Frameworks",
+					"@loader_path/Frameworks",
+				);
+				"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = (
+					"$(inherited)",
+					"@executable_path/../Frameworks",
+					"@loader_path/../Frameworks",
+				);
 				MTL_ENABLE_DEBUG_INFO = YES;
 				PRODUCT_BUNDLE_IDENTIFIER = com.dcg.ChartsTests;
 				PRODUCT_NAME = "$(TARGET_NAME)";
@@ -1145,7 +1165,8 @@
 				GCC_WARN_UNUSED_FUNCTION = YES;
 				GCC_WARN_UNUSED_VARIABLE = YES;
 				SDKROOT = macosx;
-				SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
+				SWIFT_COMPILATION_MODE = wholemodule;
+				SWIFT_OPTIMIZATION_LEVEL = "-O";
 				SWIFT_VERSION = 5.0;
 				TARGETED_DEVICE_FAMILY = "1,2,3,4";
 				VALIDATE_PRODUCT = YES;
@@ -1206,7 +1227,8 @@
 				GCC_WARN_UNUSED_VARIABLE = YES;
 				ONLY_ACTIVE_ARCH = YES;
 				SDKROOT = macosx;
-				SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
+				SWIFT_COMPILATION_MODE = wholemodule;
+				SWIFT_OPTIMIZATION_LEVEL = "-O";
 				SWIFT_VERSION = 5.0;
 				TARGETED_DEVICE_FAMILY = "1,2,3,4";
 			};
@@ -1229,7 +1251,11 @@
 				INFOPLIST_FILE = "Source/Supporting Files/Info.plist";
 				INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
 				IPHONEOS_DEPLOYMENT_TARGET = 9.0;
-				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks";
+				LD_RUNPATH_SEARCH_PATHS = (
+					"$(inherited)",
+					"@executable_path/../Frameworks",
+					"@loader_path/Frameworks",
+				);
 				MACOSX_DEPLOYMENT_TARGET = 10.11;
 				MARKETING_VERSION = 4.0.0;
 				MTL_ENABLE_DEBUG_INFO = NO;
@@ -1238,7 +1264,8 @@
 				SDKROOT = macosx;
 				SKIP_INSTALL = YES;
 				SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator appletvos appletvsimulator";
-				SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
+				SWIFT_COMPILATION_MODE = wholemodule;
+				SWIFT_OPTIMIZATION_LEVEL = "-O";
 				SWIFT_VERSION = 5.0;
 				TVOS_DEPLOYMENT_TARGET = 9.0;
 				VERSIONING_SYSTEM = "apple-generic";
@@ -1263,14 +1290,23 @@
 				GCC_NO_COMMON_BLOCKS = YES;
 				INFOPLIST_FILE = "Tests/Supporting Files/Info.plist";
 				IPHONEOS_DEPLOYMENT_TARGET = 9.0;
-				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
-				"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks";
+				LD_RUNPATH_SEARCH_PATHS = (
+					"$(inherited)",
+					"@executable_path/Frameworks",
+					"@loader_path/Frameworks",
+				);
+				"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = (
+					"$(inherited)",
+					"@executable_path/../Frameworks",
+					"@loader_path/../Frameworks",
+				);
 				MTL_ENABLE_DEBUG_INFO = NO;
 				PRODUCT_BUNDLE_IDENTIFIER = com.dcg.ChartsTests;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				SDKROOT = macosx;
 				SUPPORTED_PLATFORMS = "iphoneos iphonesimulator appletvos appletvsimulator";
-				SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
+				SWIFT_COMPILATION_MODE = wholemodule;
+				SWIFT_OPTIMIZATION_LEVEL = "-O";
 				SWIFT_VERSION = 5.0;
 				TVOS_DEPLOYMENT_TARGET = 9.0;
 			};
@@ -1307,6 +1343,25 @@
 			defaultConfigurationName = Release;
 		};
 /* End XCConfigurationList section */
+
+/* Begin XCRemoteSwiftPackageReference section */
+		228D56212554F27A00BEE75E /* XCRemoteSwiftPackageReference "swift-algorithms" */ = {
+			isa = XCRemoteSwiftPackageReference;
+			repositoryURL = "https://github.com/apple/swift-algorithms";
+			requirement = {
+				kind = upToNextMajorVersion;
+				minimumVersion = 0.0.2;
+			};
+		};
+/* End XCRemoteSwiftPackageReference section */
+
+/* Begin XCSwiftPackageProductDependency section */
+		228D56222554F27A00BEE75E /* Algorithms */ = {
+			isa = XCSwiftPackageProductDependency;
+			package = 228D56212554F27A00BEE75E /* XCRemoteSwiftPackageReference "swift-algorithms" */;
+			productName = Algorithms;
+		};
+/* End XCSwiftPackageProductDependency section */
 	};
 	rootObject = 193FC8DF32D250560C5F5D77 /* Project object */;
 }
diff --git a/Charts.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Charts.xcworkspace/xcshareddata/swiftpm/Package.resolved
new file mode 100644
index 0000000000..27daaabac3
--- /dev/null
+++ b/Charts.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -0,0 +1,25 @@
+{
+  "object": {
+    "pins": [
+      {
+        "package": "swift-algorithms",
+        "repositoryURL": "https://github.com/apple/swift-algorithms",
+        "state": {
+          "branch": null,
+          "revision": "bb3bafeca0e164ece3403a9de646b7d38c07dd49",
+          "version": "0.0.2"
+        }
+      },
+      {
+        "package": "swift-numerics",
+        "repositoryURL": "https://github.com/apple/swift-numerics",
+        "state": {
+          "branch": null,
+          "revision": "6b24333510e9044cf4716a07bed65eeed6bc6393",
+          "version": "0.0.8"
+        }
+      }
+    ]
+  },
+  "version": 1
+}
diff --git a/Package.resolved b/Package.resolved
new file mode 100644
index 0000000000..27daaabac3
--- /dev/null
+++ b/Package.resolved
@@ -0,0 +1,25 @@
+{
+  "object": {
+    "pins": [
+      {
+        "package": "swift-algorithms",
+        "repositoryURL": "https://github.com/apple/swift-algorithms",
+        "state": {
+          "branch": null,
+          "revision": "bb3bafeca0e164ece3403a9de646b7d38c07dd49",
+          "version": "0.0.2"
+        }
+      },
+      {
+        "package": "swift-numerics",
+        "repositoryURL": "https://github.com/apple/swift-numerics",
+        "state": {
+          "branch": null,
+          "revision": "6b24333510e9044cf4716a07bed65eeed6bc6393",
+          "version": "0.0.8"
+        }
+      }
+    ]
+  },
+  "version": 1
+}
diff --git a/Package.swift b/Package.swift
index f4be90e76b..8b266f9287 100644
--- a/Package.swift
+++ b/Package.swift
@@ -18,8 +18,14 @@ let package = Package(
             type: .dynamic,
             targets: ["Charts"])
     ],
+    dependencies: [
+        .package(url: "https://github.com/apple/swift-algorithms", from: "0.0.1")
+    ],
     targets: [
-        .target(name: "Charts")
+        .target(
+            name: "Charts",
+            dependencies: [.product(name: "Algorithms", package: "swift-algorithms")]
+        )
     ],
     swiftLanguageVersions: [.v5]
 )
diff --git a/Source/Charts/Data/Implementations/Standard/ChartDataSet.swift b/Source/Charts/Data/Implementations/Standard/ChartDataSet.swift
index 1d80da1632..e898682de0 100644
--- a/Source/Charts/Data/Implementations/Standard/ChartDataSet.swift
+++ b/Source/Charts/Data/Implementations/Standard/ChartDataSet.swift
@@ -9,6 +9,7 @@
 //  https://github.com/danielgindi/Charts
 //
 
+import Algorithms
 import Foundation
 
 /// Determines how to round DataSet index values for `ChartDataSet.entryIndex(x, rounding)` when an exact x-value is not found.
@@ -195,58 +196,10 @@ open class ChartDataSet: ChartBaseDataSet
     /// An empty array if no Entry object at that index.
     open override func entriesForXValue(_ xValue: Double) -> [ChartDataEntry]
     {
-        var entries = [ChartDataEntry]()
-        
-        var low = startIndex
-        var high = endIndex - 1
-        
-        while low <= high
-        {
-            var m = (high + low) / 2
-            var entry = self[m]
-            
-            // if we have a match
-            if xValue == entry.x
-            {
-                while m > 0 && self[m - 1].x == xValue
-                {
-                    m -= 1
-                }
-                
-                high = endIndex
-                
-                // loop over all "equal" entries
-                while m < high
-                {
-                    entry = self[m]
-                    if entry.x == xValue
-                    {
-                        entries.append(entry)
-                    }
-                    else
-                    {
-                        break
-                    }
-                    
-                    m += 1
-                }
-                
-                break
-            }
-            else
-            {
-                if xValue > entry.x
-                {
-                    low = m + 1
-                }
-                else
-                {
-                    high = m - 1
-                }
-            }
-        }
-        
-        return entries
+        let match: (ChartDataEntry) -> Bool = { $0.x == xValue }
+        let i = partitioningIndex(where: match)
+        guard i < endIndex else { return [] }
+        return self[i...].prefix(while: match)
     }
     
     /// - Parameters:
@@ -260,98 +213,57 @@ open class ChartDataSet: ChartBaseDataSet
         closestToY yValue: Double,
         rounding: ChartDataSetRounding) -> Int
     {
-        var low = startIndex
-        var high = endIndex - 1
-        var closest = high
-        
-        while low < high
-        {
-            let m = (low + high) / 2
-            
-            let d1 = self[m].x - xValue
-            let d2 = self[m + 1].x - xValue
-            let ad1 = abs(d1), ad2 = abs(d2)
-            
-            if ad2 < ad1
-            {
-                // [m + 1] is closer to xValue
-                // Search in an higher place
-                low = m + 1
-            }
-            else if ad1 < ad2
+        var closest = partitioningIndex { $0.x >= xValue }
+        guard closest < endIndex else { return closest }
+
+        let closestXValue = self[closest].x
+
+        switch rounding {
+        case .up:
+            // If rounding up, and found x-value is lower than specified x, and we can go upper...
+            if closestXValue < xValue && closest < index(before: endIndex)
             {
-                // [m] is closer to xValue
-                // Search in a lower place
-                high = m
+                formIndex(after: &closest)
             }
-            else
+
+        case .down:
+            // If rounding down, and found x-value is upper than specified x, and we can go lower...
+            if closestXValue > xValue && closest > startIndex
             {
-                // We have multiple sequential x-value with same distance
-                
-                if d1 >= 0.0
-                {
-                    // Search in a lower place
-                    high = m
-                }
-                else if d1 < 0.0
-                {
-                    // Search in an higher place
-                    low = m + 1
-                }
+                formIndex(before: &closest)
             }
-            
-            closest = high
+
+        case .closest:
+            break
         }
-        
-        if closest != -1
-        {
-            let closestXValue = self[closest].x
+
+        guard closest < endIndex else { return endIndex }
             
-            if rounding == .up
-            {
-                // If rounding up, and found x-value is lower than specified x, and we can go upper...
-                if closestXValue < xValue && closest < endIndex - 1
-                {
-                    closest += 1
-                }
-            }
-            else if rounding == .down
+        // Search by closest to y-value
+        if !yValue.isNaN
+        {
+            while closest > startIndex && self[index(before: closest)].x == closestXValue
             {
-                // If rounding down, and found x-value is upper than specified x, and we can go lower...
-                if closestXValue > xValue && closest > 0
-                {
-                    closest -= 1
-                }
+                formIndex(before: &closest)
             }
-            
-            // Search by closest to y-value
-            if !yValue.isNaN
+
+            var closestYValue = self[closest].y
+            var closestYIndex = closest
+
+            while closest < endIndex
             {
-                while closest > 0 && self[closest - 1].x == closestXValue
-                {
-                    closest -= 1
-                }
-                
-                var closestYValue = self[closest].y
-                var closestYIndex = closest
-                
-                while true
+                formIndex(after: &closest)
+                let value = self[closest]
+
+                if value.x != closestXValue { break }
+                if abs(value.y - yValue) <= abs(closestYValue - yValue)
                 {
-                    closest += 1
-                    if closest >= endIndex { break }
-                    
-                    let value = self[closest]
-                    
-                    if value.x != closestXValue { break }
-                    if abs(value.y - yValue) <= abs(closestYValue - yValue)
-                    {
-                        closestYValue = yValue
-                        closestYIndex = closest
-                    }
+                    closestYValue = yValue
+                    closestYIndex = closest
                 }
-                
-                closest = closestYIndex
             }
+
+            closest = closestYIndex
         }
         
         return closest
diff --git a/Source/Charts/Highlight/ChartHighlighter.swift b/Source/Charts/Highlight/ChartHighlighter.swift
index 08b75ac89b..f1f1873386 100644
--- a/Source/Charts/Highlight/ChartHighlighter.swift
+++ b/Source/Charts/Highlight/ChartHighlighter.swift
@@ -9,6 +9,7 @@
 //  https://github.com/danielgindi/Charts
 //
 
+import Algorithms
 import Foundation
 import CoreGraphics
 
@@ -73,7 +74,7 @@ open class ChartHighlighter : NSObject, Highlighter
         
         guard let data = self.data else { return vals }
         
-        for (i, set) in zip(data.indices, data) where set.isHighlightEnabled
+        for (i, set) in data.indexed() where set.isHighlightEnabled
         {
             // extract all y-values from all DataSets at the given x-value.
             // some datasets (i.e bubble charts) make sense to have multiple values for an x-value. We'll have to find a way to handle that later on. It's more complicated now when x-indices are floating point.
diff --git a/Source/Charts/Highlight/CombinedHighlighter.swift b/Source/Charts/Highlight/CombinedHighlighter.swift
index 40c254cfbf..0a03db2301 100644
--- a/Source/Charts/Highlight/CombinedHighlighter.swift
+++ b/Source/Charts/Highlight/CombinedHighlighter.swift
@@ -9,6 +9,7 @@
 //  https://github.com/danielgindi/Charts
 //
 
+import Algorithms
 import Foundation
 import CoreGraphics
 
@@ -35,10 +36,8 @@ open class CombinedHighlighter: ChartHighlighter
             let dataObjects = chart.combinedData?.allData
             else { return vals }
         
-        for i in dataObjects.indices
+        for (i, dataObject) in dataObjects.indexed()
         {
-            let dataObject = dataObjects[i]
-
             // in case of BarData, let the BarHighlighter take over
             if barHighlighter != nil && dataObject is BarChartData,
                 let high = barHighlighter?.getHighlight(x: x, y: y)
@@ -48,7 +47,7 @@ open class CombinedHighlighter: ChartHighlighter
             }
             else
             {
-                for (j, set) in zip(dataObject.indices, dataObject) where set.isHighlightEnabled
+                for (j, set) in dataObject.indexed() where set.isHighlightEnabled
                 {
                     let highs = buildHighlights(dataSet: set, dataSetIndex: j, xValue: xValue, rounding: .closest)
 
diff --git a/Source/Charts/Highlight/RadarHighlighter.swift b/Source/Charts/Highlight/RadarHighlighter.swift
index e5f087e00f..d01cb4fd6f 100644
--- a/Source/Charts/Highlight/RadarHighlighter.swift
+++ b/Source/Charts/Highlight/RadarHighlighter.swift
@@ -49,7 +49,7 @@ open class RadarHighlighter: PieRadarHighlighter
         let sliceangle = chart.sliceAngle
         let factor = chart.factor
 
-        for (i, dataSet) in zip(chartData.indices, chartData)
+        for (i, dataSet) in chartData.indexed()
         {
             guard let entry = dataSet.entryForIndex(index) else { continue }
             
diff --git a/Source/Charts/Renderers/YAxisRendererRadarChart.swift b/Source/Charts/Renderers/YAxisRendererRadarChart.swift
index a47bf6ac18..3b3cde0bf8 100644
--- a/Source/Charts/Renderers/YAxisRendererRadarChart.swift
+++ b/Source/Charts/Renderers/YAxisRendererRadarChart.swift
@@ -151,7 +151,7 @@ open class YAxisRendererRadarChart: YAxisRenderer
         let xOffset = axis.labelXOffset
 
         let entries = axis.entries[from..<to]
-        zip(entries.indices, entries).forEach { index, entry in
+        entries.indexed().forEach { index, entry in
             let r = CGFloat(entry - axis._axisMinimum) * factor
             let p = center.moving(distance: r, atAngle: chart.rotationAngle)
             let label = axis.getFormattedLabel(index)