The puzzle for Day 2 was about finding the needles in a haystack. Or the bad SKUs in the product catalogue, which really clicked to with my retail experience at Dick's Sporting Goods.
Part 1
Given list of numerical ranges, in form "x-y", find all the numeric values in the range (inclusive) that are the first half repeated. so 55 (5 twice), 6464 (64 twice), and 123123 (123 twice)
The input might look something like this
11-22,95-115,998-1012,1188511880-1188511890,222220-222224
For a solution, I started with the LongRange type. Ok, I didn't. I started with IntRange, but I eventually had to upgrade everything to LongRange. (LongRange is just more fun to say. Am I right?). So my RangeParse class was just
class RangeParser {
fun parse(string: String): LongRange {
val numbers = string.split("-")
.map { it.toLong() }
if (numbers[1] < numbers[0]) {
throw IllegalArgumentException("Invalid range")
}
return numbers[0].rangeTo(numbers[1])
}
}
Where the input is one of the substrings out of the input like "11-22" or "1188511880-1188511890". Turns out I didn't need the ordering check to make sure the second value was greater than the first, guess they choose not to throw any garbage data into this set.
Next we have to come up with a way to test each value in the range. Now you could embed the evaluation in a for loop, but that would make it harder to test. And while I'm not building these solutions test first, I always find String parsing a challenge, so I did write tests for this. Back to the question of how to evaluate the Long values for this pattern.
I decided to use the java.util.function.Predicate<T> interface for this. I think I could have done this pure Kotlin, but decided to be explicit in this case. Here's the implementation:
class PartOnePredicate : Predicate<Long> {
fun Int.isEven() : Boolean {
return this % 2 == 0
}
override fun test(t: Long): Boolean {
val stringForm = t.toString()
val l = stringForm.length
return if (l.isEven()) {
val halfLength = l/2
val firstHalf = stringForm.take(halfLength)
val secondHalf = stringForm.takeLast(halfLength)
return firstHalf == secondHalf
} else {
false
}
}
}
I would have sworn there was an "isEven" implementation in the Kotlin standard library, but I couldn't find one. So, I decided to implement my own with an extension function. And looking at this again I can see I'm making my code less readable using this single character variable names (l,t). But they aren't anything meaning, unlike the variables in the second half of the function (pardon the pun).
So, the answer to the puzzle is given a series of ranges, evaluate all the numbers in the range, idenity these "bad" numbers and sum them up across all the ranges.
Walking the range was really simple:
class RangeProcessor(private val predicate : Predicate<Long>){
fun process(range: LongRange) : Long {
return range
.filter { value -> predicate.test(value) }
.sum()
}
}
Originally, I just created the predicate instance as an attribute, but part 2 of the puzzle changed up the evaluation rules, so I added is a constructor argument. This take a range, iterates over it, runs each value the Predicate. The predicate acts as a positive filter, meaning only elements that match the filter are included in the output. Then I just had to sum up what passes the filter.
Then it was simply
val rangeProcessor = RangeProcessor(predicate)
val answer = ranges.sumOf { range -> rangeProcessor.process(range) }
Admittedly, what I wrote originally was
ranges.map { rangeProcessor.process(it) }.sum()
IntelliJ recommend the sumOf notation, which is a part of the streams API in the Kotlin standard library. Another example of the higher level abstracts that can be built on top of Java because Kotlin is effectively a DSL on top of the JDK.
Part 2
class PartTwoPredicateTest {
val predicate = PartTwoPredicate()
@DisplayName("ints with repeated values")
@ParameterizedTest(name = "{index}: {0}")
@ValueSource(ints = [ 11, 22, 99,111,1010,1188511885,565656])
fun `test returns true`(toTest : Int) {
assertEquals(true, predicate.test(toTest.toLong()))
}
@DisplayName("ints that do not have repeated values")
@ParameterizedTest(name = "{index}: {0}")
@ValueSource(ints = [ 12,121, 1234123,2345])
fun `test returns false`(toTest : Int) {
assertEquals(false, predicate.test(toTest.toLong()))
}
}
class PartTwoPredicate : Predicate<Long> {
private val partOnePredicate = PartOnePredicate()
override fun test(t: Long): Boolean {
return if (!partOnePredicate.test(t)) {
val stringForm = t.toString()
val length = stringForm.length
val halfLength : Int = length/2
val chunks = 1..halfLength
chunks.forEach { chunkSize ->
val sample = stringForm.take(chunkSize)
val regex = Regex("($sample)+")
val matches: Boolean = stringForm.matches(regex)
if (matches) {
return true
}
}
return false
} else {
true
}
}
}
No comments:
Post a Comment