diff --git a/examples/src/luhny/solution.kt b/examples/src/luhny/solution.kt new file mode 100644 index 00000000000..2132d27985c --- /dev/null +++ b/examples/src/luhny/solution.kt @@ -0,0 +1,95 @@ +package luhnybin + +import std.io.* +import java.io.* +import java.util.ArrayDeque + +fun isCardChar(it : Char) : Boolean = it == ' ' || it == '-' || it.isDigit() + +fun applyMask(a : String) : String { + val luhny = Luhny() + for (c in a) { + luhny.process(c) + } + luhny.processEnd() + + return luhny.output.toString().sure() +} + +class Luhny() { + private val buffer = ArrayDeque() + private val digits = ArrayDeque(16) + + private var toBeMasked = 0 + val output = StringBuilder() + + fun process(it : Char) { + buffer.addLast(it) + + when { + it.isDigit() -> digits.addLast(it.toDigit()) + it == ' ' || it == '-' -> { + } + else -> printAll() + } + + if (digits.size() > 16) { + printUntil { it.isDigit() } + digits.removeFirst() + } + + check() + } + + fun processEnd() { + while (digits.size() >= 14) { + check() + printUntil { it.isDigit() } + digits.removeFirst() + } + + printAll() + } + + private fun check() { + val size = digits.size() + if (size < 14) return + + var sum = 0 + var i = 0 + for (d in digits) { + sum += if (i % 2 == size % 2) double(d) else d + i++ + } + + if (sum % 10 == 0) + toBeMasked = size + } + + private fun double(d : Int) = d * 2 / 10 + d * 2 % 10 + + fun printAll() { + printUntil { false } + digits.clear() + } + + private fun printUntil(f : (Char) -> Boolean) { + while (!buffer.isEmpty()) { + val c = buffer.removeFirst() + printChar(c) + if (f(c)) return + } + } + + private fun printChar(c : Char) { + if (c.isDigit() && toBeMasked > 0) { + output.append('X') + toBeMasked-- + } else { + output.append(c) + } + } +} + +fun Char.isDigit() = Character.isDigit(this) +fun Char.toDigit() = this.int - '0'.int diff --git a/examples/src/luhny/test.kt b/examples/src/luhny/test.kt new file mode 100644 index 00000000000..d4500670081 --- /dev/null +++ b/examples/src/luhny/test.kt @@ -0,0 +1,260 @@ +package luhnybin + +import java.util.* + +fun main(args : Array) { + run() + System.out?.println("Success") +} + +// HELPER FUNCTIONS + +val MASK : Char = 'X' + +val MIN_LENGTH : Int = 14 +val MAX_LENGTH : Int = 16 + +val random = Random(0xDEADBEEF) + +class test(val testName : String) { + var input : String = "" + + fun send(input : String) : test { + this.input = input + return this + } + + fun expect(expected : String) { + val actual = applyMask(input) + + assertEquals( + actual, expected, + "${testName} failed on \"${input}\"\n" + + "applyMask(data) = \"${actual}\", but must be \"$expected\"") + } + + fun sendAndExpect(input : String) = send(input).expect(input) + + fun assertEquals(actual : T?, expected : T?, message : Any? = null) { + if (actual != expected) { + if (message == null) + throw AssertionError() + else + throw AssertionError(message) + } + } +} + +fun run() { + test("line feed preservation").sendAndExpect("LF only ->\n<- LF only") + + for (i in MIN_LENGTH..MAX_LENGTH) + test("valid " + i + "-digit #") + .send(randomNumber(i)) + .expect(mask(i)) + + for (i in MIN_LENGTH..MAX_LENGTH) + test("non-matching " + i + "-digit #").sendAndExpect(nonMatchingSequence(i)) + + test("not enough digits").sendAndExpect(nonMatchingSequence(MIN_LENGTH - 1)) + + var tooMany = nonMatchingSequence(MAX_LENGTH) + tooMany += computeLast(tooMany) + test("too many digits").sendAndExpect(tooMany) + + test("14-digit # prefixed with 0s") + .send("00" + randomNumber(14)) + .expect(mask(16)) + + test("2 non-matching digits followed by a 14-digit #") + .send("1256613959932537") + .expect("12XXXXXXXXXXXXXX") + + test("14-digit # embedded in a 16-digit #") + .send(nestedNumber()) + .expect(mask(16)) + + test("16-digit # flanked by non-matching digits") + .send("9875610591081018250321") + .expect("987XXXXXXXXXXXXXXXX321") + + testFormatted(' ') + testFormatted('-') + + test("exception message containing a card #") + .send("java.lang.FakeException: " + formattedNumber(' ') + " is a card #.") + .expect("java.lang.FakeException: " + formattedMask(' ') + " is a card #.") + + test("non-matching message").sendAndExpect("4111 1111 1111 111 doesn't have enough digits.") + + test("non-matching message").sendAndExpect("56613959932535089 has too many digits.") + + test("sequence of zeros") + .send(repeatingSequence('0', 1000)) + .expect(mask(1000)) + + test("long sequence of non-digits").sendAndExpect(nonDigits()) + + testOverlappingMatches() + + test("long sequence of digits with no matches").sendAndExpect(nonMatchingSequence(1000)) +} + +fun formatNumber(number : String, delimiter : Char) : String { + if (number.length != 16) + throw IllegalArgumentException("Expected length of 16.") + + val formatted = StringBuilder() + for (i in 0..3) { + formatted.append(number.substring(i * 4, (i * 4) + 4)) + if (i < 3) + formatted.append(delimiter) + } + + return formatted.toString() ?: "" +} + +fun formattedNumber(delimiter : Char) : String = formatNumber(randomNumber(16), delimiter) + +fun checkDigit(c : Char) : Char { + if (c < '0' || c > '9') + throw IllegalArgumentException("Not a digit: " + c) + + return c +} + +fun intValue(c : Char) : Int = checkDigit(c) - '0' + +/** Computes the last digit necessary to pass the Luhn check. */ +fun computeLast(allButLast : String) : Char { + var sum = 0 + + var i = allButLast.length - 1 + while (i >= 0) { + val value = intValue(allButLast[i]) shl 1 + sum += if (value > 9) value - 9 else value + + i -= 2 + } + + var j = allButLast.length - 2 + while (j >= 0) { + sum += intValue(allButLast[j]) + j -= 2 + } + + var remainder = sum % 10 + return if (remainder == 0) '0' else ('0' + (10 - remainder)).chr +} + +fun computeLast(allButLast : CharSequence) : Char = computeLast(allButLast.toString().sure()) + +fun setRandomDigits(builder : StringBuilder, start : Int, end : Int) { + for (i in start..end-1) + builder.setCharAt(i, randomDigit()) +} + +/** Generates a random digit. */ +fun randomDigit() : Char = ('0' + random.nextInt(10)).chr + +fun nonDigits() : String { + val nonDigits = StringBuilder() + for (i in 0..999) + nonDigits.append((random.nextInt(68) + ':').chr) + return nonDigits.toString().sure() +} + +fun testFormatted(delimiter : Char) { + test("16-digit # delimited with '" + delimiter + "'") + .send(formattedNumber(delimiter)) + .expect(formattedMask(delimiter)) +} + +fun formattedMask(delimiter : Char) : String { + val mask = StringBuilder() + + for (i in 0..3) { + mask.append(repeatingSequence(MASK, 4)) + if (i < 3) + mask.append(delimiter) + } + + return mask.toString().sure() +} + +/** Computes a random, valid card # with the specified number of digits. */ +fun randomNumber(digits : Int) : String { + val number = StringBuilder(digits) + number.setLength(digits) + setRandomDigits(number, 0, digits - 1) + number.setCharAt(digits - 1, computeLast(number.subSequence(0, digits - 1).sure())) + return number.toString() ?: "" +} + +/** Creates a 16-digit card # with a 14-digit number embedded inside. */ +fun nestedNumber() : String { + val number = StringBuilder(16) + number.setLength(16); + setRandomDigits(number, 0, 14); + number.setCharAt(14, computeLast(number.subSequence(1, 14).sure())) + number.setCharAt(15, computeLast(number.subSequence(0, 15).sure())) + return number.toString().sure() +} + +/** Creates a sequence of mask characters with the given length. */ +fun mask(length : Int) : String { + return repeatingSequence(MASK, length); +} + +/** Creates a sequence of c with the given length. */ +fun repeatingSequence(c : Char, length : Int) : String { + val sb = StringBuilder() + for (i in 1..length) + sb.append(c) + return sb.toString().sure() +} + +class DigitSet() { + val bitSet = BitSet() + + fun add(digit : Char) = bitSet.set(intValue(digit)) + fun contains(digit : Char) : Boolean = bitSet.get(intValue(digit)) + fun clear() = bitSet.clear() +} + +fun testOverlappingMatches() { + val output = StringBuilder(randomNumber(MAX_LENGTH)) + for (i in 0..1000 - MAX_LENGTH - 1) + output.append(computeLast(output.subSequence(i + 1, i + MAX_LENGTH).sure())) + + test("long sequence of overlapping, valid #s") + .send(output.toString().sure()) + .expect(mask(output.length())); +} + +/** Generates a sequence of digits with the specified length and no card #s. */ +fun nonMatchingSequence(length : Int) : String { + val builder = StringBuilder() + + for (lastIndex in 0..length-1) { + val excluded = DigitSet() + excluded.clear(); + + // Compute digits that would result in valid card #s. + for (subLength in MIN_LENGTH..MAX_LENGTH) { + val start = lastIndex - (subLength - 1); + if (start < 0) + break; + excluded.add(computeLast(builder.subSequence(start, lastIndex).sure())); + } + + // Find a digit that doesn't result in a valid card #. + var digit : Char = '0' + do { + digit = randomDigit() + } while (excluded.contains(digit)) + builder.append(digit); + } + + return builder.toString().sure() +}