2

I am curious if there is a way in swift to achieve the most closest value via their modern api's?

For example:

let x = [1.2, 3.4, 4.5, 6.7, 8.9]
print(x.getClosestValue(3.7) //3.4

I have been playing around with map and reduce but still not be able to crack this problem. The problem occur's that I have to iterate through entire array to detect false positives as well. There can be a scenario where you can have multiple closest values, so was just wondering how this can be done in swiftly way?

topgun
  • 2,463
  • 8
  • 31
  • 46
  • 1
    check this answer: https://stackoverflow.com/a/51806138/8476915 – Moayad Al kouz Nov 25 '19 at 15:39
  • 1
    is the list always sorted? – m1sh0 Nov 25 '19 at 15:43
  • 1
    Yes this list is always sorted. – topgun Nov 27 '19 at 05:29
  • If the list is sorted, you can use the approaches I mentioned in my answer, but you can also do it more efficiently with a divide and conquer strategy, like a binary search. Do you want to know the range of **all the closest values** or just **one closest value**? I can provide more details on how to implement this divide and conquer approach - it would take a couple of lines, though. – Marcel Alves Nov 27 '19 at 11:52

4 Answers4

4

You can use min(by:) to achieve this and it doesn't require a sorted array

let x = [1.2, 3.4, 4.5, 6.7, 8.9]
let target = 3.7

let closestTarget = x.min(by: {abs($0 - target) < abs($1 - target)})
Joakim Danielson
  • 43,251
  • 5
  • 22
  • 52
1

I can imagine some different scenarios, so I will try to address most of them.


1- You want to find only one number:

1.1 - Finding the actual number:

You can use min(by:):

let x = [1.2, 4.0, 3.4, 6.7, 8.9]
let target = 3.7
let closestValue = x.min { abs($0 - target) < abs($1 - target) }
print(closestValue) // prints Optional(4.0)

This approach is the most straightforward. You will get the result that returns the minimum value of the subtraction between the array element and the target.

1.2 - Finding the index:

You can also use min(by:), but first, you get the enumerated version of the array to get the indices.

let x = [1.2, 4.0, 3.4, 6.7, 8.9]
let target = 3.7
let closestIdx = x.enumerated().min { abs($0.1 - target) < abs($1.1 - target) }!.0
print(closestIdx) // prints 1

Note: Even though 3.4 is equally distant to 3.7 as 4.0, this approach will always return 4.0 as the answer because of floating-point arithmetic (you can check this blog post if you're interested in this topic).


2- You want to find all the closest numbers:

Since you've mentioned that there can be multiple numbers, I think this would be the method of your choice.

2.1 - Finding all closest numbers:

let x = [1.2, 3.4, 4.0, 6.7, 8.9]
let target = 3.7
let minDiff = x.map { return abs($0 - target) }.min()!
let closestValues = x.filter { isDoubleEqual(a: $0, b: target - minDiff) || isDoubleEqual(a: $0, b: target + minDiff) }
print(closestValues) // prints [3.4, 4.0]

The difference here is that we use filter() to find all the values that are equally distant to a target. There may be repeated values, which can be eliminated using a Set if you wish.

2.2 - Finding the indices of all closest numbers:

The same idea of using enumerated() again to take the indices.

let x = [1.2, 3.4, 4.0, 6.7, 8.9]
let target = 3.7
let minDiff = x.map { return abs($0 - target) }.min()!
let tuples = x.enumerated().filter { isDoubleEqual(a: $0.1, b: target - minDiff) || isDoubleEqual(a: $0.1, b: target + minDiff) }
let closestIndices = tuples.map { return $0.0 }
print(closestIndices) // prints [1, 2]

Note: isDoubleEqual(a: Double, b: Double) -> Bool is a function that returns true if the values a and b are considered equal according to floating-point arithmetic. See this post for more information - but note that you should adjust the epsilon to a value that you find suitable.


The complexity of these solutions is O(n).

A final note: if you have an array that is already sorted, as mentioned by other answers, you can take advantage of this property to find what you want using a binary search.

Community
  • 1
  • 1
Marcel Alves
  • 486
  • 1
  • 8
  • 15
0

This solution using reduce(_:_:) should work:

let x = [1.2, 3.4, 4.5, 6.7, 8.9]
let target = 4.7

// assumes x is non-empty array
let closestTarget = x.reduce(x[0]) { closest,val in
    abs(target - closest) > abs(target - val) ? val : closest
}
Evan Deaubl
  • 709
  • 6
  • 10
  • Don't misuse `reduce` when other, better-fitting functions exist. In this case, you're doing a `max(by:)` operation. https://github.com/amomchilov/Blog/blob/master/Don't%20abuse%20reduce.md – Alexander Nov 27 '19 at 14:47
-1

There is no straight api from Apple to use yet. If you don't care about the time, you can sort the array and use Array's first(where:) or last(where:) methods to do linear search. However, you can make it better by sort and using binary search, it will probably just take you additional 20 lines?

Developer Sheldon
  • 2,140
  • 1
  • 11
  • 17
  • `sorted()`+ `first(where:)` works, but it's `O(n*log(n))`. You can achieve `O(n)` by just using `min(by:)`, `max(by:)`, without even needing to implement binary search. Though of course binary search would be best, achieving `O(log(n))` (assuming the input is already sorted) – Alexander Nov 25 '19 at 16:01
  • @Alexander-ReinstateMonica why you can assume your input is sorted? I sorted it first of course because I don't assume it is sorted. lol. – Developer Sheldon Nov 27 '19 at 01:59
  • `min(by:)`/`max(by:)` doesn't require sorting. Your proposed use of `first(where:)` or binary search *would* require sorting. – Alexander Nov 27 '19 at 14:45
  • @Alexander-ReinstateMonica thanks for calling it out. i have to admit my solution is pretty bad. lol – Developer Sheldon Dec 03 '19 at 22:04