Luhny Code Challenge. Proposed by Bob Lee. Ported to Kotlin by Leonid Shalupov.
This commit is contained in:
@@ -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
|
||||
@@ -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()
|
||||
}
|
||||
Reference in New Issue
Block a user