Luhny Code Challenge. Proposed by Bob Lee. Ported to Kotlin by Leonid Shalupov.

This commit is contained in:
Andrey Breslav
2012-01-27 21:00:33 +04:00
parent d524d058f0
commit b8ccee7bdf
2 changed files with 355 additions and 0 deletions
+95
View File
@@ -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<Char>()
private val digits = ArrayDeque<Int>(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
+260
View File
@@ -0,0 +1,260 @@
package luhnybin
import java.util.*
fun main(args : Array<String>) {
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<T>(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()
}