Canonical Hex

So today, in the context of assembling color components, I decided that a hex value should always be padded to 2, 4, 8, 16, 32, or 64 characters when building out chunks.

Under this system, the number 256 should be “0100” and not “100”, representing the smallest standard memory footprint that will store the value. This allows chunks to be losslessly assembled together because there’s a consistent width for each component. If you need to use a larger padding value, you can re-pad to a larger minimum width.

While RGB colors come in 6’s, those are (in my opinion) three channels of 2-digit hex numbers, not a six-digit hex number.

Interesting things about this implementation:

  • You have to use Swift.max because Int has its own max symbol.
  • I was initially convinced to drop strtol() in favor of the built-in radix initializer, but this does do away with leading 0x support, which you have to strip yourself. (I did not include stripping in this implementation.) In my SwiftString repo, I’ve reverted to the old-style conversions.
  • Greg T’s version with fls instead of my convoluted log2 stuff is vastly improved, so I’ve substituted it in-place here.
  • My goal is to produce full-width number-chunks that can be assembled to represent components, whether for storing information, for channels, or for clarity in reading. An RGB group of (0xFF, 0x7, 0xFF) must be “FF07FF”, not “FF7FF”. So yes, there will be leading zeroes. If you want to interpret  output as numbers, use a canonical prefix (0x).
extension String {
    /// Left pads string to at least `minWidth` characters wide
    public func leftPad(_ character: Character, toWidth minWidth: Int) -> String {
        guard minWidth > characters.count else { return self }
        return String(repeating: String(character), 
            count: minWidth - characters.count) + self
    /// Returns String's hex value
    /// - note: Non-compliant strings default to 0
    /// - note: An earlier version used strtol(), 
    ///   which accepts leading "0x". This does not.
    public var hexValue : Int { return Int(self, radix: 16) ?? 0 }

public extension Int {
    /// Returns Int's representation as hex string using 0-padding
    /// to represent the smallest standard memory footprint that can
    /// store the value.
    public var hexString : String {
        let unpaddedHex = String(self, radix:16, uppercase: true)
        let stringCharCount = unpaddedHex.characters.count
        // thanks, Greg Titus
        let desiredPadding = 1 << Swift.max(fls(Int32(stringCharCount - 1)), 1) 
        return unpaddedHex.leftPad("0", toWidth: Int(desiredPadding))

As always, corrections, improvements, suggestions welcome.


  • I would:

    – Only convert Int->String once.
    – Use fls() to get the index of the high-order bit of one-less-than-the-length in order to determine the power of 2 to use, instead of floating point logarithms. fls() usually gets converted to a single instruction.
    – I haven’t fixed it, but I notice that a value of 0 results in “0” instead of the expected “00”. And similarly, what ought to be done for negative Ints? Maybe just define this on UInt instead, and put a guard self != 0 else { return “00” } first?

        public var hexString : String {
            let unpaddedHex = String(self, radix:16, uppercase: true)
            let stringCharCount = unpaddedHex.characters.count
            let desiredPadding = 1 << fls(Int32(stringCharCount-1))
            return unpaddedHex.leftPad("0", toWidth: Int(desiredPadding))
  • Not sure where my comment went (I know, even to me the commenting system is a mystery) but you missed adding the max(_, 1) on fls

    • Oops! Yes, my mistake. I’d rather check for 0 explicitly with a guard here, but that’s why I got the wrong answer for zero, yup.

  • I think a better API would be for hexValue to be of type Optional Int. That way, failed conversions aren’t silent. Plus, nil coalescing is so easy in Swift if a client really needed `foo.hexValue ?? 0`.

    • I was heading that way and I ended up going back to strtol, etc, because I like having the prefix support baked in.

  • A lot of languages, and users with backgrounds in those languages, treat a number with a leading 0 as octal. So your example 0100 will be treated as 0x40. May not matter in a purely swift context, but it sure matters if you have a team with varied backgrounds.

    • These are chunks, not final units. I also provide access to prefixes in the SwiftStrings project so a proper number can be assembled. My problem was lossiness among the chunks, not their interpretation. Allowing leading zeroes mandates either a prefix or a well-defined system such as the 3-tuple 2-character hex sequence for RGB numbers.

  • Erica, sorry, this is unrelated to this actual post… But I have a suggestion/request for you, to help out the Swift laggards, Objective-C hanger-ons amongst us: a re-work of your iOS Drawing Code on github, to a modern Swift 3 approach, showing how to best use Swift language feature and modern design. Interested? Cheers.

    • If I could find a way to monetize, I’d be there 100% but Pearson/Addison Wesley still owns the book and doesn’t want to update it to Swift

  • Odd. Why? I have never seen leading zeroes in numbers of any radix so why here? Useless. Unwise. To quote somebody. This way you introduce an exception for hex numbers that does not exist for numbers in other radices.

    • That’s now how I’m using them. My use case is to provide chunkable full-width units that can be joined together without loss. The SwiftStrings project offers prefixes so there’s no issue about interpretation.

  • We can use `String(format:)` to eliminate leftPad and `flsl` to remove ugly `Int32` casts:

        public var hexString : String {
            let minimalLength = Swift.max(flsl(self) - 1, 0) / 4 + 1
            let desiredLength = 1 << flsl(minimalLength - 1)
            return String(format: "%0\(desiredLength)X", self)