Nick Jones

iOS Developer

Lists of numbers

This problem was asked by Twitter:

Started 11:53

"Given a list of numbers, create an algorithm that arranges them in order to form the largest possible integer

For example, given [10, 7, 76, 415], you should return 77641510"

So first thing's first let's set up a super basic testable version of all of this before we start even thinking about the solution just so we have it out of the way:

let integerList = [10, 7, 76, 415]
func integerMasher(with list: [Int]) -> Int {0} 
let isValid = integerMasher(with: integerList) == 77641510

So this gives us a way to know if what we're doing works which is great

⚠️ Heads Up

Given that we'll be returning an integer it could be worth checking what the largest integer that swift supports is.

This is something that is probably out of scope and maybe irrelevant so we'll first solve the problem and then loop back round to this


Initial thoughts

So as we're dealing with an array we'll have to iterate at least once no matter what. Swift language buit in functions like reduce, map, etc loop under the hood so there's no escaping this

Initially my thoughts were that the first digit in a given number is the most important in terms of where it 'ranks' irrelevant of how big the number 'actually' is"

What I mean by this is even though 899 is clearly bigger than 9 in terms of normal integers; with just these two numbers in our given scenario a result of 9899 is larger than 8999 when combining 899 and 9 in the available combinations for these two integers

This is interesting because does it mean that the shorter the number in terms of digits the 'greater' it is in terms of where it should sit in the final number?

Not always now that I think of it because what really matters is the "up-front" digits

I wonder if an 'easy' solution would be to 'fill' any numbers with less than three digits with 9s and then sort by those numbers

i.e. with our original numbers 7 would become 799 and 76 would become 769.

We could then just store both the original number and the 'modified' number, sort by the 'modified' number and then combine all of the 'original' numbers together

⚠️ Heads Up

This isn't very efficient because we'll have to do a number of loops

We'll also have to take into account the greatest 'length' of a number in the array in terms of digits so as to 'fill' the rest of the numbers up to the same amount

For fun though we can implement this first and maybe it'll give us some hints as to a better approach along the way!


func masher(with list: [Int]) -> Int {
    guard let longestNumber = list.sorted(by: {
        String($0).count > String($1).count 
    }).first else { return 0 }

    let longestNumberLength = String(longestNumber)
    return 0
}

Now that I've started implementing this I've realised how much 'overhead' is really required to get this solution working

Not only do we have to sort our array but we have to convert every value to a String only to then get the first element out, convert it to a String and then from there start converting every value in the array

Whilst the solution will 'work' it feels janky even as a 'working' solution so let's drop it temporarily and approach it differentl

With just raw integers themselves I don't think there's a way of sorting these as the larger numbers will always break any logic we have, that is to say that 111 is far greater than 9 and there's no esacping that

It would be great to be able to get the length of these numbers without having to cast them to strings

If we could do that then we could start making some real progress!


Next steps

I started checking online for a mathematical based approach and came across the fact that the log10 of a number gives you the length of the number in digits like in this situation:

let singleDigitNumber: CGFloat = 1
print(log10(singleDigitNumber))
0

let tripleDigitNumber: CGFloat = 100
print(log10(tripleDigitNumber))
2

This is great as it helps out with our solution!

⚠️ Heads Up

I loved maths at school but I never took advanced maths or what we'd call "A Level" maths in the UK

So with that in mind whilst this function will help us create a solution for this particular problem we'll come back to this in another coding challenge to understand, at least on a basic level, how logarithms work and so why log10 gives us this result

For now though we can use this to come up with a solution

It's an interesting problem because the longer the number the less 'likely' it is that it'll be near the front of our end number, 9999998 comes after 9 for example, but we can't just filter by 'length' because 9999998 still comes before 8 🤔

Maybe our original masher solution is the best way to start this off in the end!

Despite looping back round to our original idea we do now have a much nicer way of getting the 'longest' number as well as the length of it because of log10 so let's go again

Because we want to make use of log10 (which is a function of CGFloat) a lot we'll start by first recasting our array of Integers so that we're not constantly recasting the same values unnecessarily

func masher(with list: [Int]) -> Int {
    let floatArray = list.compactMap { CGFloat($0) }
}

Now we want to get the longest number in terms of digits and from that get its length so we can use this to pad out our other numbers to match its length

guard let longestNumber = floatArray.sorted(by: { 
    log10($0) > log10($1) 
}).first else { return 0 }
let longestNumberLength = log10(longestNumber)

Now that we've got the greatest length we can start building our our custom list with both properties

We'll make a Struct to store these numbers as it's a bit more 'swifty'

struct NumberBox {
    let originalNumber: Int
    let modifiedNumber: Int
}

So with our new Struct to store our values and our initial approach let's go ahead and start looping over our new `floatArray`

var numberBoxes = [NumberBox]()
for float in floatArray {
  //All of the following logic will take place in here 
}

The first thing we'll want to do is if a number is already at the 'max' length or the same length as the longest number in the array then we'll just leave it as i and create a NumberBox object with both values the same

guard log10(float) != longestNumberLength else {
    numberBoxes.append(NumberBox(
        originalNumber: Int(float),
        modifiedNumber: Int(float)
    ))
    continue
}

Next we want to turn each float into a String so that we can then start "padding" it with 9s

We can achieve this padding by using the aptly named padding function available through Swift's String built-in struct!

This function needs three things; a desired length of String, i.e. how long you want the final string to be, what string the function should use to fill out or pad your string with and lastly an index of where the function should start inserting said string

let numberAsString = String(Int(float))

let modifiedNumber = numberAsString.padding(
    toLength: Int(longestNumberLength),
    withPad: "9",
    startingAt: numberAsString.count
)

In our case here we always want each string (A string represenation of our "Number" that is) to have the same length as the largest number that we could find in our array.

We then want to always pad the number with 9s as that was the key to our solution as we figured out earlier

And then most importantly we want the function to start adding these 9s to the end of our string / number

Once we have this padded out number we can then add it to our growing list of NumberBox objects!

if let paddingStringAsInt = Int(modifiedNumber) {
    numberBoxes.append(NumberBox(
        originalNumber: Int(float),
        modifiedNumber: paddingStringAsInt
    ))
}

So now we have all of our numbers and we can sort by their 'modified' values!

let sortedNumbers = numberBoxes.sorted(by: { 
    $0.modifiedNumber > $1.modifiedNumber 
})

And then finally we can take our sorted array, compactMap our original numbers into an array of strings by casting each one, joining then via Swift's built-in joined() function and then recasting that large number back into an integer

return Int(sortedNumbers.compactMap {
    String($0.originalNumber)
}.joined()) ?? 0

Putting it all together we have something like this

var numberBoxes = [NumberBox]()
    for float in floatArray {
        guard log10(float) != longestNumberLength else {
            numberBoxes.append(NumberBox(
                originalNumber: Int(float),
                modifiedNumber: Int(float)
            ))
            continue
        }

        let numberAsString = String(Int(float))

        let modifiedNumber = numberAsString.padding(
            toLength: Int(longestNumberLength),
            withPad: "9",
            startingAt: numberAsString.count
        )

        if let paddingStringAsInt = Int(modifiedNumber) {
            numberBoxes.append(NumberBox(
                originalNumber: Int(float),
                modifiedNumber: paddingStringAsInt
            ))
        }
    }

let sortedNumbers = numberBoxes.sorted(by: { 
    $0.modifiedNumber > $1.modifiedNumber 
})

return Int(sortedNumbers.compactMap {
    String($0.originalNumber)
}.joined()) ?? 0
            

Ok let's give it a spin and see what we get!

masher(with: integerList) == 77641510

💥 Uh oh it's crashing!

If we take a lot our crash logs we'll see that we're getting back the following

Terminating app due to uncaught exception 
  'NSInvalidArgumentException', reason: '*** 
-[NSTaggedPointerString 
stringByPaddingToLength:withString:startingAtIndex:]: 
out of range padIndex'

Luckily for us in this case reading the error explains the error clearly

Here we're clearly being informed that the index we're passing to the padding function is out of bounds in exactly the same way as if were to try and reference an object in array using an index greater than the size of the array

⚠️ Heads Up

A related topic to this are subscripts which String leverages when you do something such as:

let someString = "ohYoureApproachingMe"
let firstChar = someString[firstCharacter.startIndex]

It's a really interesting topic and a powerful tool that you can make use of yourself in particular sitations. You can read more about them directly on the Swift language page:

Read about Subscripts

Back to our crash though and we need to add some safety barriers around what we pass into the padding function to prevent it blowing up in our face

So let's add some catches for this so that if the starting index is less than 0 we'll just use 0

let startingIndex = min(numberAsString.count - 1, 0)

Great our crash is all gone!

But our primitive test still isn't passing

Let's debug this either by our preferred method whether that be sticking a breakpoint at a relative point, printing out some of our values throughout the function or seeing the values of these variables via the results sidebar in Playgrounds

I used Playgrounds for all of my coding challenges as it's the most lightweight way to get to us executing Swift code

With that in mind when I take a look at our results I can see that our sortedNumbers array consists of a three digit number (415) as our largest number in terms of digit length but that our smaller numbers such as 7 are only being bumped up to 79 rather than the expected 799

If we take a look at our padding code again with this knowledge the problem becomes apparent!

let startingIndex = min(numberAsString.count - 1, 0)
let modifiedNumber = numberAsString.padding(
    toLength: Int(longestNumberLength),
    withPad: "9",
    startingAt: startingIndex
)

Here we're asking the padding function to pad our string to a certain length which is non-zero indexed as it's a count but when we're passing it our desired length we're passing it a zero-index value through longestNumberLength

To keep things simple we can simply offset this with something like this

let startingIndex = min(numberAsString.count - 1, 0)
let modifiedNumber = numberAsString.padding(
    toLength: Int(longestNumberLength) + 1,
    withPad: "9",
    startingAt: startingIndex
)

Now let's run our code again

Success!

This is great so let's add some more test scenarios to make sure that it still works with varying lengths of digits as well as smaller array sizes

let normalSet = [1, 2, 9, 899, 991]
let bigCombine = [999, 998, 99, 9]
let twoDigits = [98, 9, 12]
let fourDigits = [9878, 56, 99, 1]
let bigDigits = [999998, 9]

masher(with: normalSet) == 999189921 
masher(with: bigCombine) == 999999998 
masher(with: twoDigits) == 99812 
masher(with: fourDigits) == 999878561 
masher(with: bigDigits) == 9999998 

Looks like our tests are all running A-ok and that, overall, it works

It's not very efficient mind you but it's a first working solution

Normally without any outside help, where we can come to a working initial solution within a reasonable amount of time, we should do so!

Tidying up

Whilst keeping our same solution there are plenty of things we can do to not only improve the efficiency of our code but also the legibility

Let's start with this part first

return Int(sortedNumbers.compactMap {
    String($0.originalNumber)
}.joined()) ?? 0

Whilst it seems innocuous at first (Fancy word I know) it's something that we can easily improve upon to make our code more efficient

Previously we had the following code:

let numberAsString = String(Int(float))

let modifiedNumber = numberAsString.padding(
    toLength: Int(longestNumberLength),
    withPad: "9",
    startingAt: numberAsString.count
)

Later on however we're then adding an integer representation of our number when creating our NumberBox object

if let paddingStringAsInt = Int(modifiedNumber) {
    numberBoxes.append(NumberBox(
        originalNumber: Int(float),
        modifiedNumber: paddingStringAsInt
    ))
}

But then later on re-casting this Int into a String whenever we're doing this

return Int(sortedNumbers.compactMap {
    String($0.originalNumber)
}.joined()) ?? 0

Alternatively let's just store our originalNumber as a String

struct NumberBox {
    let originalNumber: String
    let modifiedNumber: Int
}

And now we no longer recast for every object in our array!

return Int(sortedNumbers.compactMap {
    $0.originalNumber
}.joined()) ?? 0

⚠️ Heads Up

Whilst this change may seem insignifcant the concept and thought process that it represents is far more valuable

In our original situation here not only were we running a very basic operation of casting an Int to a String but we were also doing so for only a handful of objects

This means that the difference in performance between these two in this specific example is negligible

When dealing with conversions between larger and more complex objects however the performance difference starts to grow so always keep in mind where you can make changes to limit casts where possible especially when dealing with iterative code such as for loops

This is our next prime target:

var numberBoxes = [NumberBox]()

Where possible we should avoid mutability and prefer constants and immutability

Immutable code creates a more concrete codebase and reduces the chance that we'll run into issues where we modify the properties of an object in one place only to modify again elsewhere giving us unpredictable software

One initial thought might be to tackle it with closure initialisation

let numberBoxes: [NumberBox] = {
    //Generate some boxes here
}()

Whilst this is a step in the right direction we still can't escape our mutability issue as we'll need a way of storing our NumberBox objects as we iterate through our floats

let numberBoxes: [NumberBox] = {
    var boxesToBuild = [NumberBox]()
    for float in floatArray {
        //Create a NumberBox
        //Add it to our boxesToBuild
    }
    return boxesToBuild
}()

What we really need is to create a function that will give us a NumberBox when we pass it both an original number and a maximum length of digits

func numberBox(forFloat float: CGFloat, 
          withMaxLength length: Double) -> NumberBox? {

    let floatAsInt = Int(float)
    guard log10(float) != longestNumberLength else {
        return NumberBox(
            originalNumber: String(floatAsInt),
            modifiedNumber: floatAsInt
        )
    }
    
    let numberAsString = String(floatAsInt)
    let modifiedNumber = numberAsString.padding(
        toLength: Int(longestNumberLength) + 1,
        withPad: "9",
        startingAt: min(numberAsString.count - 1, 0)
    )
            
    guard let paddingStringAsInt = Int(modifiedNumber) else { 
       return nil
    }
    return NumberBox(
        originalNumber: numberAsString,
        modifiedNumber: paddingStringAsInt
    )
}

This is the exact code we were using before except instead of appending our NumberBox objects to mutable array we're just returning a NumberBox object

We can then take this function and plug it into a compact map to generate our array of NumberBox objects!

let numberBoxes = floatArray.compactMap {
    numberBox(forFloat: $0, withMaxLength: longestNumber)
}

⚠️ Heads Up

This function doesn't have to live outside of this function on its own for it to exist

Just like we did with our NumberBox struct you can define and use functions within functions which is useful in situations just like this

Keep in mind that isolating functions and objects within functions like this makes them both un-reusable and untestable

The lack of resuability and testability doesn't however mean you should always avoid this. There are plenty of situations where this setup is fine such as with our sitation here

In our situation here we have no use for the NumberBox object outside of the intMasher function and the same is also true for our new numberBox(forFloat, withMaxLength) function.

Both of these entities exist solely to improve the readability and safety of our function and their testing status is covered by the fact that we're testing the only place in which they exist

For our last final touch let's fix this "not so pretty" sorted function

let sortedNumbers = numberBoxes.sorted(by: {
    $0.modifiedNumber > $1.modifiedNumber }
)

By taking our existing NumberBox object we can simply update it to conform to the Comparable protocol

When we conform to the Comparable protocol we need to provide both implementations for the lesser than function as well as the equals function

For our NumberBox object however all of our properties are already Equatable (String / Int) and so Swift can infer whether two NumberBox objects are equal by default

We still need to tell Swift how it should deal with lesser than for our object however:

struct NumberBox: Comparable {
    static func < (lhs: NumberBox, rhs: NumberBox) -> Bool {
        lhs.modifiedNumber > rhs.modifiedNumber
    }
    
    let originalNumber: String
    let modifiedNumber: Int
}

Lucky for us Swift can also infer greater than for us also since it's just the exact opposite of what we're explicitly defined!

End result

And with that here's our end result!

func intMasher(with list: [Int]) -> Int {
    
  let floatArray = list.compactMap { CGFloat($0) }
  
  guard let longestNumber = floatArray.sorted(by: { 
    log10($0) > log10($1) 
  }).first else { return 0 }
  let longestNumberLength = log10(longestNumber)
  
  struct NumberBox: Comparable {
    static func < (lhs: NumberBox, rhs: NumberBox) -> Bool {
        lhs.modifiedNumber > rhs.modifiedNumber
    }
      
    let originalNumber: String
    let modifiedNumber: Int
  }
  
  func numberBox(forFloat float: CGFloat,
              withMaxLength length: Double) -> NumberBox? {
      let floatAsInt = Int(float)
      guard log10(float) != longestNumberLength else {
          return NumberBox(
              originalNumber: String(floatAsInt),
              modifiedNumber: floatAsInt
          )
      }
      
      let numberAsString = String(floatAsInt)
      let modifiedNumber = numberAsString.padding(
          toLength: Int(longestNumberLength) + 1,
          withPad: "9",
          startingAt: min(numberAsString.count - 1, 0)
      )
              
      guard let asInt = Int(modifiedNumber) else { 
        return nil
      }
      return NumberBox(
          originalNumber: numberAsString,
          modifiedNumber: asInt
      )
  }
  
  let numberBoxes = floatArray.compactMap {
     numberBox(forFloat: $0, withMaxLength: longestNumber)
  }
  
  let sortedNumbers = numberBoxes.sorted()

  return Int(sortedNumbers.compactMap {
      $0.originalNumber
  }.joined()) ?? 0
}

let integerList = [10, 7, 76, 415]
let normalSet = [1, 2, 9, 899, 991]
let bigCombine = [999, 998, 99, 9]
let twoDigitOnly = [98, 9, 12]
let fourDigit = [9878, 56, 99, 1]
let bigDigits = [999998, 9]

intMasher(with: integerList) == 77641510
intMasher(with: normalSet) == 999189921
intMasher(with: bigCombine) == 999999998
intMasher(with: twoDigitOnly) == 99812
intMasher(with: fourDigit) == 999878561
intMasher(with: bigDigits) == 9999998

(As a heads up I've reduced some of the indentation to prevent having to horizontally scroll to read the code)

Whilst we might not necessarily want to settle with this solution in the real world, if we had no one else to help us figure out how to do this "properly" initially, getting something working gives us not only a good base from where to start with improvements but also a better understanding of the problem, which more often than not, can help lead us to a better solution

Finished 13:54

Total time 2:01


Post solution deep dive 🤿

One of the things we wanted to test out was Int overflows; that is, if we give the function a huge array of numbers the Int will just become too big for Swift to handle

📙 About deep-dive sections

Within these deep-dive sections I'll be going into a lot more detail around topics related to parts of the task based on both my existing knowledge and what I've learnt as part of the task

These deep-dives don't serve as "next steps" or "improvements" to the existing solution to the task but more of a supplementary section where I can explain and go further into very specific topics that would be over the top as part of the main challenge

So with that if you're interested in the topic read on. Alternatively if you're primarily interested in the challenge itself this is where that part ends

What happens currently with our function?

let hugeNumbers = [999, 999, 999, 999, 999, 999]
masher(with: hugeNumbers) == 999999999999999999

And it still works

this is becuse the biggest number supported by Int in Swift on a 64 bit machine is a whopping 9223372036854775807

But why this particular number?

The maximum number that can be stored by a given integer is determined by the given system you are running your code on

If you're running your code on an 8 bit system then, as the name suggests, the maximum integer value is defined by exactly that, the number of bits available to the processor

A bit is either 1 or 0, on or off and so with this in mind we can calculate the maximum size of an int on a given system by 2^x where x is the number of bits available minus 1

📙 Further reading

Something key to remember is that this number relates to the "maximum" an integer can be and not the total range as whatever our maximum integer value is we can also support the negative or "inverse" of this number so for a 32 bit system -2147483648 (not 2147483647)

For what it's worth I know that these bounds exist and all of what I've explained but I don't understand, at a fundamental level, the reasoning for the difference between the upper and lower bounds and why it exists

The concept itself is something called "Two's Complement" and so if diving deeper is something you're interested in you could start here:

Two's Complement on Wikipedia

So with all of that in mind

"what happens if I go above or below these numbers?"

In general (Not in Swift by default) going above or below a system's minimum or maximum range causes what's referred to as an "overflow"

When you overflow "positively" on, say, an 8 bit system where the largest number is 127; if we were to add 1 to our number we would then have -128

The reverse is also true when we "negatively" overflow. So if we minus 1 from -128 we'll be given 127

In fact this exacty problem has been the root of a number of different video game glitches in the past with one of the more famous ones being the Final Fantasy damage overload glitch!

FFVII Damage Overflow Glitch

In swift however by default overflows are handled as errors when using our traditional methods of addition, subtraction or multiplication

For example if we were to create a variable with the maximum integer value and try to increase it:

var reallyBigInt = 9223372036854775807
reallyBigInt += 1

At runtime what we'll run into is a crash with the following error

* thread #1, queue = 'com.apple.main-thread', 
  stop reason = Swift runtime failure: arithmetic overflow

Taking this even further, if we statically try to set an integer to something that would overflow Swift won't even let us compile!

For example the following:

let reallyBigInt = 9223372036854775808

Gives us this error when trying to compile:

Integer literal '9223372036854775808' 
  overflows when stored into 'Int'

⚠️ Heads Up

Keep in mind that when we're assigning these integers, without explicitly typing them, Swift's type inferrence is inferring them, as default, to Int

In Swift the Int type is platform dependant which means that whilst the following will be fine on my 64 bit machine:

let reallyBigInt = 9223372036854775807

On a 32 bit machine this code wouldn't even compile! Let alone the fact that it would crash at runtime

Whilst Apple no longer offers support for new 32 bit applications it is something worth keeping in mind as if you find you need to support both you can explicitly type your variables and constants like such:

let reallyBig32BitInt: Int32 = 2147483647

And to test out that this truly does only support the maximum 32 bit value you can try out the following:

let tooBig32BitInt: Int32 = 9223372036854775808

Which when you try and compile will give you this nice error:

Integer literal '9223372036854775807' 
  overflows when stored into 'Int32'

Going back to Swift and its default overflow behaviour is it even possible for us to overflow in Swift without a crash?

And the answer is yes!

Whilst our default methods of addition, subtraction and multiplication in Swift will cause crashes such as in the following example:

let biggest32BitInt: Int32 = 2147483647
let smallest32BitInt: Int32 = -2147483648

let additionOverflow = biggest32BitInt + 1         //💥
let subtractionOverflow = smallest32BitInt - 1     //💥
let multiplicationOverflow = biggest32BitInt * 2   //💥

There is a way to make use of the overflow concept in Swift and that's through the built-in overflow operators!

let biggest32BitInt: Int32 = 2147483647
let smallest32BitInt: Int32 = -2147483648

let additionOverflow = biggest32BitInt &+ 1       
// result: -2,147,483,648

let subtractionOverflow = smallest32BitInt &- 1  
// result: 2,147,483,647

let multiplicationOverflow = biggest32BitInt &* 2
// result: -2

Now that we have all of this figured out the final piece of the puzzle is the concept of unsigned integers!

Unsigned integers at the most basic level are simply integers that are always non-negative

We can see this in action by trying to create a negative unsigned 32 bit integer

let negative32BitUInt32: UInt32 = -1

Which when we try and compile will cause us to be greeted with a lovely error

Negative integer '-1' 
  overflows when stored into unsigned type 'UInt32'

Similar to our scenario from before, when we try and overflow an unsigned integer using Swift's default operators such as in this scenario:

let negative32BitUInt32: UInt32 = 0
let subtractionOverflow = negative32BitUInt32 - 1

We run yet again into our previous error

* thread #1, queue = 'com.apple.main-thread', 
  stop reason = Swift runtime failure: arithmetic overflow

What makes unsigned integers powerful in the right hands however is that they are able to store a larger positive value for the tradeoff of not being able to hold negative values

In fact as you might have guessed you're able to store both the sum of the positive and negative "amounts" in an unsigned integer as we can see here

let max32BitInt = Int32.max
// result: 2,147,483,647

let max32BitUInt = UInt32.max
// result: 4,294,967,295

As unsigned integers can't store negative values if we apply our overflow operator like we did before and attempt to reach a negative value we'll overflow to the maximum value of the unsigned integer; and if we were to do the same at the upper bounds we'd end up back at 1

let negative32BitUInt32: UInt32 = 0
let subtractionOverflow = negative32BitUInt32 &- 1
// result: 4,294,967,295

let positive32BitUInt32: UInt32 = 4294967295
let additionOverflow = negative32BitUInt32 &+ 1
// result: 1

With this in mind it's easy to see how when used in the wrong place you could easily run into issues with unsigned integers whereby something may be reduced to be less than 0 and suddenly you end up with a huge unexpected number!

This potentially unexpected behaviour is the main reason why overflowing isn't supported by Swift's default operators and instead exists as an "opt-in" feature


Despite being a surface level explanation of Integers and overflows I hope this was at least either somewhat interesting or insightful and at best only the first part of your own deep dive into Integers!