코틀린 입문 스터디 (13) 실습 : Rationals, Board

mook2_y2
15 min readMar 7, 2019

--

스터디파이 코틀린 입문 스터디 (https://studypie.co/ko/course/kotlin_beginner) 관련 자료입니다.

코틀린 입문반은 Kotlin을 직접 개발한 개발자가 진행하는 Coursera 강좌인 “Kotlin for Java Developers” (https://www.coursera.org/learn/kotlin-for-java-developers) 를 기반으로 진행되며 아래는 본 강좌 요약 및 관련 추가 자료 정리입니다.

목차

(1) Introduction

(2) From Java to Kotlin

(3) Basics

(4) Control Structures

(5) Extensions

(6) 실습 : Mastermind game

(7) Nullability

(8) Functional Programming

(9) 실습 : Mastermind in a functional style, Nice String, Taxi Park

(10) Properties

(11) Object-oriented Programming

(12) Conventions

(13) 실습 : Rationals, Board

(14) Inline functions

(15) Sequences

(16) Lambda with Receiver

(17) Types

(18) 실습 : Game 2048 & Game of Fifteen

1. Rationals

문제 설명

  • rational number (유리수) : 두 정수의 비율로 나타낼 수 있는 수 (단, 분모는 0이 아님)
  • numerator(분자), denominator(분모)에 제한이 없는 값을 저장하기 위해 java.math.BigInteger 클래스 사용합니다.
  • 객체 생성 시점에서 분모가 0인 경우 IllegalArgumentException 예외를 발생시켜야 합니다.
  • 분모를 반올림 없이 정확한 값들로 저장해야합니다. (floating-point numbers (부동소수점 수)과 다름)
  • Rational class에 대한 산술/비교 연산자를 (ex: +, -, *, /, ==, <. >=) 구현해야 합니다.
  • 비교시에는 정규화된 형태 (normalized form)로 비교되어야합니다. (ex: 2/4 == 1/2) & 분모가 1인 경우 생략
  • String으로 변환시에 정규화된 형태가 출력되어야 합니다.
  • 정규화를 위해 BigInteger.gcd (두 BigInteger간의 최대공약수 반환하는 메소드) 를 사용할 수 있습니다.
  • Rational class 객체 생성 방법은 1) String to Rational, 2) divBy infix operator (대상 타입 : Int, Long, BitInteger) 로 2가지 입니다.
  • 실습시 고려할점들은 1) Int, Long, BitInteger가 상속하는 Number 부모클래스를 Receiver & argument type으로 사용, 2) 1주차에 배운 확장함수 사용, 3) 1주차에 배운 when expression 사용, 4) 클래스에 대한 특징을 나타내고 및 빈번히 사용되는 속성을 프로퍼티로 처리, 연산을 멤버 메소드로 처리, 5) operator 키워드를 이용한 연산자 오버로딩, 6) in 키워드 커스텀을 위해 간소화된 RationalRange 및 contains 메소드 구현 입니다.

예시 답안 및 설명

// 1. Int, Long, BitInteger가 상속하는 Number 부모클래스를 Receiver & argument type으로 사용
infix fun Number.divBy(other: Number): Rational {
if (other == 0) { throw IllegalArgumentException("denominator must not be zero") }

fun checkArgument(num: Number):BigInteger = when(num){
is Int, is Long -> num.toLong().toBigInteger()
is BigInteger -> num
else -> throw IllegalArgumentException("must be of types Int, Long, or BigInteger")
}

return Rational(
numerator = checkArgument(this),
denominator = checkArgument(other)
)
}

fun String.toRational(): Rational {
return when{
this.contains('/') -> Rational(
this.substringBefore('/').toBigInteger(),
this.substringAfter('/').toBigInteger()
)
else -> Rational(this.toBigInteger(), 1.toBigInteger())
}

}


class Rational(val numerator:BigInteger, val denominator:BigInteger){
// 2. 재사용되는 속성과 연산에 대해 프로퍼티와 멤버 메소드 구현
private val isNormalized:Boolean get(){
return numerator.gcd(denominator).compareTo(1.toBigInteger()) == 0
}
private val normalized:Rational get(){
val gcd = numerator.gcd(denominator)
return Rational(numerator/gcd, denominator/gcd)
}
private fun getLcmOfDenominators(other: Rational):BigInteger {
val gcd = this.denominator.gcd(other.denominator)
return this.denominator.multiply(other.denominator).divide(gcd)
}


//산술연산자
operator fun plus(other: Rational):Rational{
val numerator = this.numerator * (getLcmOfDenominators(other)/this.denominator) + other.numerator * (getLcmOfDenominators(other)/other.denominator)
return Rational(numerator, getLcmOfDenominators(other))
}
operator fun minus(other: Rational):Rational{
val numerator = this.numerator * (getLcmOfDenominators(other)/this.denominator) - other.numerator * (getLcmOfDenominators(other)/other.denominator)
return Rational(numerator, getLcmOfDenominators(other))
}
operator fun times(other: Rational):Rational{
return Rational(this.numerator * other.numerator, this.denominator * other.denominator) }
operator fun div(other: Rational):Rational{
return Rational(this.numerator * other.denominator, this.denominator * other.numerator)
}

//단항연산자
operator fun unaryMinus():Rational{ return Rational(-this.numerator, this.denominator) }

//비교연산자
operator fun compareTo(other: Rational):Int {
val num1 = this.numerator * (getLcmOfDenominators(other)/this.denominator)
val num2 = other.numerator * (getLcmOfDenominators(other)/other.denominator)
return num1.compareTo(num2)
}
override fun equals(other: Any?): Boolean {
return if (other is Rational){
(this.normalized.numerator == other.normalized.numerator) &&
(this.normalized.denominator == other.normalized.denominator)
} else false // nullable type check

}

//range
operator fun rangeTo(other: Rational):RationalRange { return RationalRange(this, other) }

//toString
override fun toString(): String {
return when{
this.isNormalized && denominator.compareTo(1.toBigInteger()) == 0 -> numerator.toString()
this.isNormalized && denominator.compareTo(1.toBigInteger()) != 0 -> "$numerator/$denominator"
else -> this.normalized.toString()
}
}
}

// 3. in 키워드 처리를 위한 간소화된 RationalRange 클래스 구현
class RationalRange(val start: Rational, val endInclusive: Rational){
private val gcd:BigInteger get() = start.denominator.gcd(endInclusive.denominator)
private val lcm:BigInteger get() = start.denominator.multiply(endInclusive.denominator).divide(gcd)

operator fun contains(element: Rational):Boolean {
return ( element.numerator * (this.lcm) > start.numerator * element.denominator) &&
( element.numerator * (this.lcm) < endInclusive.numerator * element.denominator )
}
}

2. Board

문제 설명

  1. SquareBoard 구현체 관련
  • 정사각형 (width * width) 모양의 칸(Cell)들을 포함하는 보드입니다.
  • start index는 0이 아니라 1부터 시작합니다.
  • createSquareBoard 메소드를 통해 보드 객체 생성합니다.
  • getCellOrNull()은 해당 index의 Cell 있을 경우 반환하고 없으면 Null을 반환합니다.
  • getCell()은 해당 index의 Cell 있을 경우 반환하고 없으면 IllegarArgumentException 에러를 발생시킵니다.
  • getRow()는 고정된 x축에 대한 y구간을 리스트 타입으로 반환합니다.
  • getColumn()은 고정된 y축에 대한 x구간을 리스트 타입으로 반환합니다.
  • getNeighbour()은 Direction Enum을 받아서 이에 대응되는 Cell을 반환합니다
  • 모든 메소드는 새로운 객체가 아니라 기존 Cell을 반환합니다.

2. GameBoard 구현체 관련

  • 보드 Cell에 Value를 저장하고 수정 및 조회 기능이 제공됩니다.
  • SquareBoard 구현체를 상속하여 구현합니다.
  • Cell 클래스는 Gameboard가 저장하는 value와 독립적으로 존재해야 합니다.

3. 실습시 고려할점

  • cells를 공유하기 위해 top-level로 선언합니다. (kotlin에서 정적 변수 선언하는 방법 3가지 중 하나)
  • cells에 불필요한 Nullability 이슈 제거하기 위해 late initialization을 사용합니다. (무조건 호출 전에 초기화된다고 가정)
  • Functional Programming 파트에서 배운 Collection Operations를 통해 함수형 스타일 & 간소화 코드로 생성 및 조회 로직을 처리합니다.
  • init { } 을 통해 생성자 로직을 커스텀해야 합니다.
  • Enum과 when 을 사용합니다.

예시 답안 및 설명

lateinit var cells:List<Cell>


fun createSquareBoard(width: Int): SquareBoard {
return SquareBoardImpl(width)
}

fun <T> createGameBoard(width: Int): GameBoard<T> {
return GameBoardImpl(width)
}

open class SquareBoardImpl(override val width: Int) : SquareBoard{
init{
cells = createCells()
}

private fun createCells(): List<Cell>{
return (1 .. width).flatMap { i ->
(1 .. width).map{ j ->
Cell(i, j)
}
}
}

override fun getAllCells(): Collection<Cell> { return cells }

override fun getCell(i: Int, j: Int): Cell {
return cells.first{cell ->
cell.i == i && cell.j == j
}
}

override fun getCellOrNull(i: Int, j: Int): Cell? {
return cells.firstOrNull{cell ->
cell.i == i && cell.j == j
}
}

override fun getColumn(iRange: IntProgression, j: Int): List<Cell> {
return iRange.mapNotNull { i ->
getCellOrNull(i, j)
}
}

override fun Cell.getNeighbour(direction: Direction): Cell? {
return when(direction){
Direction.UP -> getCellOrNull(this.i-1, this.j)
Direction.DOWN -> getCellOrNull(this.i+1, this.j)
Direction.RIGHT -> getCellOrNull(this.i, this.j+1)
Direction.LEFT -> getCellOrNull(this.i, this.j-1)
}
}

override fun getRow(i: Int, jRange: IntProgression): List<Cell> {
return jRange.mapNotNull { j ->
getCellOrNull(i, j)
}
}
}


class GameBoardImpl<T>(width: Int) : GameBoard<T>, SquareBoardImpl(width) {
private val cellValueMap: MutableMap<Cell, T?>

init{
cellValueMap = createValueMap()
}

private fun createValueMap():MutableMap<Cell, T?> {
return cells.map{
it to null
}.toMap().toMutableMap()
}


override fun get(cell: Cell): T? {
return cellValueMap[cell]
}

override fun set(cell: Cell, value: T?) {
cellValueMap[cell] = value
}

override fun filter(predicate: (T?) -> Boolean): Collection<Cell> {
return cellValueMap.filterValues(predicate).keys
}

override fun find(predicate: (T?) -> Boolean): Cell? {
return cellValueMap.filterValues(predicate).keys.firstOrNull()
}

override fun any(predicate: (T?) -> Boolean): Boolean {
return cellValueMap.filterValues(predicate).count() > 0
}

override fun all(predicate: (T?) -> Boolean): Boolean {
return cellValueMap.filterValues(predicate).count() == cellValueMap.count()
}

}

--

--