0

The script is to colorize nicks on IRC.

Here is the JavaScript:

<script type="text/javascript">
    function clean_nick(nick) {
        nick = nick.toLowerCase();
        nick = nick.replace(/[^a-z]/g, "")

        return nick;
    }

    function hash(nick) {
        var cleaned = clean_nick(nick);
        var h = 0;

        for(var i = 0; i < cleaned.length; i++) {
            var a = cleaned.charCodeAt(i)
            var b = (h << 6)
            var c = (h << 16)
            var d = h

            h = a + b + c - d;
        }

        console.log(h);

        return h;
    }

    function get_color(nick) {
        var nickhash = hash(nick);
        var deg = nickhash % 360;
        var h = deg < 0 ? 360 + deg : deg;
        var l = 50;

        if(h >= 30 && h <= 210) {
            l = 30;
        }

        var s = 20 + Math.abs(nickhash) % 80;

        return "hsl(" + h + "," + s + "%," + l + "%)";
    }

    console.log(get_color("kIrb-839432-`~#$%^&*()_+{}|:<>?/,'.;[]=-_-"));
</script>

And here is a Swift playground:

//: Playground - noun: a place where people can play

import UIKit

extension Character {
    func unicodeScalarCodePoint() -> UInt32 {
        let characterString = String(self)
        let scalars = characterString.unicodeScalars

        return scalars[scalars.startIndex].value
    }
}

func cleanNick(nick: String) -> String {
    let lowerCaseNick = nick.lowercased()
    let noNumberNick = lowerCaseNick.components(separatedBy: CharacterSet.decimalDigits).joined()
    let noPunctuationNick = noNumberNick.components(separatedBy: CharacterSet.punctuationCharacters).joined()
    let noSymbolNick = noPunctuationNick.components(separatedBy: CharacterSet.symbols).joined()
    let finishedNick = noSymbolNick.replacingOccurrences(of: " ", with: "")

    return finishedNick
}

func hash(nick: String) -> Int {
    let cleaned = cleanNick(nick: nick)
    var h = 0

    for char in cleaned {
        let a = char.unicodeScalarCodePoint()
        let b = (h << 6)
        let c = (h << 16)
        let d = h

        h = Int(Int(a) + Int(b) + Int(c) - Int(d))
    }

    print(h)

    return h
}

func coloredNick(nick: String) -> String {
    let nickHash = hash(nick: nick)
    let deg = nickHash % 360
    let h = deg < 0 ? 360 + deg : deg
    var l = 50

    if (h >= 30 && h <= 210) {
        l = 30
    }

    let s = 20 + abs(nickHash) % 80

    return "hsl(\(h),\(s),\(l))"
}

print(coloredNick(nick: "kIrb-839432-`~#$%^&*()_+{}|:<>?/,'.;[]=-_-"))

Both should output the same hash, and therefore should output the same hsl(), but something is going wrong in the calculation. Can't figure out where it's going wrong but the hash isn't calculating properly.

Any help is greatly appreciated.

eskimo
  • 101
  • 10
  • 1
    Do not post links. [Edit] your question to include relevant code as text in your question. Clearly explain what issues you are having with the translation. – rmaddy Jan 07 '18 at 17:35
  • Edited, the issue is just the hash isn't calculating the same even though the math is the same. – eskimo Jan 07 '18 at 17:41
  • What's the expected output? – MynockSpit Jan 07 '18 at 17:47
  • The expected output of the Swift code is the output of the JS, so: -1501579218 - hsl(222,38%,50%) – eskimo Jan 07 '18 at 17:48
  • 1
    Not familiar with Swift, but I imagine that the output of `charCodeAt()` and `characterString.unicodeScalars[i].value` is not the same. `charCodeAt()` returns a UTF-16 output and `unicodeScalars` are UTF-32. – MynockSpit Jan 07 '18 at 18:03
  • That part produces the same result in both. – eskimo Jan 07 '18 at 18:49
  • 1
    I haven't read through your code in detail, but another possible issue is the number of bits and overflows. Since the default number of bits might differ between JS and Swift based on the device you are testing on and the way of handling overflows is also different, you should also look at the intermediate results in both pieces of code and see where do they differ. – Dávid Pásztor Jan 07 '18 at 20:06

2 Answers2

3

All things you need to consider is written in the comment of Dávid Pásztor.

This is my version of hash function. It produces the same result as I tested with Safari for your JavaScript code:

func hash(_ nick: String) -> Int64 {
    let cleaned = cleanNick(nick)
    var h: Int64 = 0

    for a in cleaned.utf16 {
        let b = Int32(truncatingIfNeeded: h) &<< 6
        let c = Int32(truncatingIfNeeded: h) &<< 16
        let d = h

        h = Int64(a) + Int64(b) + Int64(c) - d
    }
    print(h) //-> -1501579218 (JS:-1501579218)

    return h
}

Some notes:

  • In JavaScript, all numbers are in 64-bit floating point, Double in Swift.
  • In JavaScript, shift operations converts all input values into 32-bit signed integer (Int32) and then converts the result into 64-bit floating point.

So, I first wrote my hash with Double and after confirming all intermediate values are integer, made it into Int64. (Int is 32-bit long in 32-bit systems and your hash may generate a value which 32-bit integer cannot contain.)

Your way of splitting up the characters may generate different result from your JavaScript code, as in the comment, but you filters the input by limiting the characters within "a"..."z".

But your filtering code is hard to understand and may generate different result from as expected.

func cleanNick(_ nick: String) -> String {
    var result = nick.lowercased()
    result = String(result.unicodeScalars.filter{"a" <= $0 && $0 <= "z"})

    return result
}
OOPer
  • 47,149
  • 6
  • 107
  • 142
  • Ahhhh very cool. Works exactly as expected. Also yeah, my cleanNick was a mess, I'm still new to iOS dev. Thank you very much :) – eskimo Jan 08 '18 at 16:07
1

Bitwise operators in javascript behave as if they are applied to 32 bit signed integers (This is explained here.) but normal operators act as if they are applied to to 64-bit floating points. Thus the << operations must be done on Int32s and the + & - operators on Int64s.

So to reflect the javascript behaviour you change the translation of the hash function to:

func hash(nick: String) -> Int64 {
    var h: Int64 = 0

    for a in cleanNick(nick: nick).utf16 {
        let b = Int32(truncatingIfNeeded: h) &<< 6
        let c = Int32(truncatingIfNeeded: h) &<< 16
        let d = h

        h = Int64(a) + Int64(b) + Int64(c) - d
    }

    print(h)

    return h
}

Side notes

String filtering

You can use custom character sets in combination with the filter method to clean the nick:

func cleanNick(nick: String) -> String {
    let lowercased = nick.lowercased().unicodeScalars
    return String(
        lowercased.filter( CharacterSet(charactersIn: "a" ... "z").contains )
    )
}

Use constants

It's not mandatory in Swift to assign and declare a constant/variable in the same statement. So the following code from coloredNick(nick: String) -> String

var l = 50 // Declares variable l and assigns it to 50

if (h >= 30 && h <= 210) {
    l = 30
}

Can be written with l as a constant instead of a variable

let l: Int // Declares constant l

if (h >= 30 && h <= 210) {
    l = 30
} else {
    l = 50
}
Damiaan Dufaux
  • 4,427
  • 1
  • 22
  • 33
  • Have you checked what happens when you call `hash(nick: "zzzz")`? The JavaScript code returns `-2536702720` for `hash("zzzz")`. Remember addition/subtraction operators in javascript behave as if they are applied to 64-bit floating point. – OOPer Jan 07 '18 at 22:51
  • changed the code to compensate the Double behaviour – Damiaan Dufaux Jan 07 '18 at 23:13