I'm doing the codingame exercises and I managed to finish the ASCII Art problem, it was long and difficult but I did it. Now I read one of the solutions and it was done in 5 lines of code! I'm trying to understand it but I'm failing to do so, can someone help me with a step by step explanation?
The test gives you an input of:
Line 1: the width L of a letter represented in ASCII art. All letters are the same width.
Line 2: the height H of a letter represented in ASCII art. All letters are the same height.
Line 3: The line of text T, composed of N ASCII characters.
Following lines: the string of characters ABCDEFGHIJKLMNOPQRSTUVWXYZ? Represented in ASCII art.
This is one of the solutions:
fun main(args : Array<String>) {
val L = readLine()!!.toInt()
val H = readLine()!!.toInt()
val T = readLine()!!.toUpperCase().replace("[^A-Z]".toRegex(), "[")
val rows = (0 until H).map{readLine()!!}
(0 until H).map{h -> T.fold(""){ a, c -> a + rows[h].substring((c-'A')*L, (c-'A')*L+L) }}.forEach{println(it)}
}
I'll be very grateful for your help :)
PS. I'm not sure what does .fold and .map.
Let's analyze line by line.
val L = readLine()!!.toInt()
Reads an Int
and puts into variable L
.
val H = readLine()!!.toInt()
Reads another Int
and puts into variable H
.
val T = readLine()!!
.toUpperCase()
.replace("[^A-Z]".toRegex(), "[")
Reads a String
, converts all its letters to uppercase characters (so a
becomes A
), replace anything that is not a letter with the [
character (the [^A-Z]
syntax is from RegEx, not Kotlin) and puts into variable T
.
val rows = (0 until H).map{ readLine()!! }
Creates a list with numbers from 0 to H - 1 and, for each number, transforms it into a String
provided by the user (the map
function transforms a list of type T1
to a list of type T2
; in this case, it transforms a list of Int
, created by 0 until H
, into a list of String
, which is the result of readLine()!!
). Puts the list into the variable rows
.
(0 until H).map{ h ->
T.fold(""){ a, c ->
a + rows[h].substring((c-'A')*L, (c-'A')*L+L)
}
}
.forEach{ println(it) }
Creates a list with numbers from 0 to H - 1 and, for each number h
, transforms it into a String
.
Before explaining how this String
is created, let's first analyze what's happening inside fold
.
The fold
receiver (i.e. the variable that is before the .
when calling fold
) is a String
, since the variable T
is a String
.
There are two arguments, a
and c
, where a
is a String
(same type of the receiver) and c
is a Char
(because String
is a collection of Char
s). We can only use fold
in collections, and when doing so, the first argument will be same type of its receiver and the second argument will be the same type of its receiver elements.
There is an implicit return, which is the argument a
concatenated with a substring of the h
-th element of the variable rows
(since rows
is a List<String>
, as we seen above, any element of this list will be a String
; also consider that a) the first element of this list is in the 0th position of this list and so on, and b) the notation x[i]
being the i
-th element of list x
; that's how Kotlin manages list access).
This substring starts at position (c - 'A') * L
and ends at position (c - 'A') * L + L
. To understand it, think of Char
as a number. So, when you write 'A', actually you're writing 65. That's a convention named ASCII. So when you do 'A' - 'A'
, you're doing 65 - 65, that's 0. Same goes for 'B' - 'A'
, that means 66 - 65 (since 'B'
is one position ahead of A
in ASCII), that's 1. Since L
is also a Int
, just replace it into the expression and you'll end up with a Int
.
But what are these two arguments, a
and c
? fold
is nothing but a loop. In the first iteration, a == ""
(the value between parenthesis after calling fold
) and c == T[0]
(i.e. the first Char
of the variable T
, which is the receiver). Some value is returned (in this case, the value of a
concatenated with the substring of rows[h]
) and, in the next iteration, a == <returned value by a and T[0]>
and c == T[1]
. In the next iteration, a == <returned value by a and T[1]>
and c == T[2]
. The loop is repeated until all the elements of T
are processed.
Finally, in the forEach
, each String
created by fold
is printed. If still in doubt, just print each variable in the screen and try to understand:
(0 until H).map{ h ->
println("h=$h")
T.fold(""){ a, c ->
println("a=$a, c=$c")
println("c - 'A' = ${c - 'A'}")
a + rows[h].substring((c-'A')*L, (c-'A')*L+L)
}
}
Hope you get the idea.
EDIT: I tried to put the response in a comment but I think it was too long, so I've made an edit.
For better understanding, let's define a function printAscii
, which is your original function, but with arguments being the inputs. Don't worry if you don't know what a function is.
fun printAscii(L: Int, H: Int, T: String, rows: List<String>) {
(0 until H).map{ h ->
T.fold(""){ a, c ->
a + rows[h].substring((c-'A')*L, (c-'A')*L+L)
}
}.forEach{ println(it) }
}
You can call a function like this:
printAscii(L = 1, H = 2, T = "some string", rows = listOf("string1", "string2"))
In this case, L
will be equal to 1
, H
will be equal to 2
, T
will be equal to "some string"
and rows
will be equal to listOf("string1", "string2")
. Now, let's play with it:
printAscii(L = 1, H = 1, T = "ABCDE", rows = listOf("12345")) // the output is 12345
printAscii(L = 1, H = 1, T = "AACDE", rows = listOf("12345")) // the output is 11345
printAscii(L = 1, H = 1, T = "ABCBA", rows = listOf("12345")) // the output is 12321
printAscii(L = 1, H = 1, T = "EDCBA", rows = listOf("12345")) // the output is 54321
printAscii(L = 1, H = 1, T = "AAAAA", rows = listOf("12345")) // the output is 11111
You can see that T
variable defines how rows
will be printed.
Now, let's change the L
variable to 2
.
printAscii(L = 2, H = 1, T = "ABCDE", rows = listOf("1234567890")) // the output is 1234567890
printAscii(L = 2, H = 1, T = "AACDE", rows = listOf("1234567890")) // the output is 1212567890
printAscii(L = 2, H = 1, T = "ABCBA", rows = listOf("1234567890")) // the output is 1234563412
printAscii(L = 2, H = 1, T = "EDCBA", rows = listOf("1234567890")) // the output is 9078563412
printAscii(L = 2, H = 1, T = "AAAAA", rows = listOf("1234567890")) // the output is 1212121212
Now you can see that the character A
in the string T
is related to the substring 12
in the string in rows
, the character B
in T
is related to the substring 34
in rows
and so on. When we changed L
to 2, each character in T
is related to a substring of size 2 in rows
(you can try with L
equals to 3 if you want, but make sure to add 5 characters in the first element of rows
, which is the size of the string T
).
Generally speaking, when H
is equal to 1
, each character in T
is related to a subtring of size L
of the first element of rows
(remember this!). But how to calculate which substring belongs to some character?
Notice that,
when L == 1
, A
is related to the substring starting at 0
, B
is related to the substring starting at 1
, C
is related to the substring starting at 2
and so on;
when L == 2
, A
is related to the substring starting at 0
, B
is related to the substring starting at 2
, C
is related to the substring starting at 4
and so on.
Can you guess the pattern for L == 3
? The pattern is simple: calculate the distance between some letter to A
(for example, the distance between A
and B
is 1, the distance between A
and E
is 4). Multiply this distance by L
and get the start position of the substring.
So, for example, when L == 2
, C
is related to the substring starting at 4. Applying our method, the distance between C
and A
is 2. Multiplying by L
, which is 2, we have 2 * 2 = 4, which is the start position of the substring.
Since the substring has a size of L
, as we discovered above, to get the final position of the substring, we can add L
to the start position.
Based on the information above, we can write the Kotlin code.
how to calculate the distance between some character c
and 'A'
? Answer: c - 'A'
how to get the start position of the substring? Answer: (c - 'A') * L
how to get the end position of the substring? Answer: (c - 'A') * L + L
how to get a substring from the first element of rows
? Answer: rows[0].substring((c - 'A') * L, (c - 'A') * L + L)
This is for H == 1
, but you can increase its value and lead to the same conclusion.
For your second question, you can take a look at this site, specially at the section Negated Character Classes. Regex has nothing to do with Kotlin, but you can use Regex in Kotlin code.
Simply speaking, [^A-Z]
in Regex means "match a single character that is not between A and Z", as well [^0-9]
means "match a single character that is not between 0 and 9". If you want to match a single character that is between A and Z, just remove the caret, i.e. [A-Z]
.
That being said, .replace("[^A-Z]".toRegex(), "[")
means "match a single character that is not between A and Z and replace this character with [
". Why [
? Remember that 'B' - 'A' == 1
and 'E' - 'A' == 4
? If you follow the train, you'll see that 'Z' - 'A' == 25
. The character right next 'Z'
in the ASCII table is [
, so '[' - 'A' == 26
. Because of that, any character c
that is not between A and Z will evaluate the expression c - 'A'
to 26.