오늘은 지난번에 만들었던 print_table 메서드에 색상 옵션을 인수로 받아 처리할 수 있도록 프로그램을 수정해 보자.
메서드에 인수로 옵션에 대한 정보를 전달할 때 해시를 사용하면 편한데 아래 간단한 예제를 보자.
print_numbers 메서드는 두 개의 파라미터를 받는데 그 중 options 파라미터는 옵션 정보를 받기 위한 파라미터로 비어있는 해시({})를 기본값으로 지정했다.
메서드 내용을 보면 먼저 별다른 옵션을 주지 않고 메서드를 호출했을 때 기본으로 적용할 옵션 정보를 담은 해시를 default_options 변수에 할당했다.
그리고 default_options 해시에 인수로 받은 옵션 정보가 담긴 options 해시를 merge 메서드를 사용해 적용하고 있다.
옵션은 출력할 대상 숫자를 필터링하는 조건(condition)과 정렬(sort) 두 가지이고 이 옵션에 따라 data 배열에 대해 select 메서드와 sort 메서드를 사용하여 최종 출력할 배열을 만들어 낸다.
sort 메서드를 사용해서도 내림차순(desc)으로 정렬할 수 있지만, 여기서는 sort 메서드가 오름차순으로 정렬한 배열을 단순히 reverse 메서드를 사용하여 역순으로 재배열했다.
이제 print_numbers 메서드 정의 아래에 있는 print_numbers 메서드 호출 예제 코드를 보자.
옵션에 대한 인수 없이 print_numbers 메서드를 호출하면 기본 옵션에 따라 단순히 입력받은 숫자 배열을 오름차순으로 정렬하여 출력한다.
condition 옵션 값을 'even' 으로 주고 호출하면 짝수만 오름차순으로 정렬하여 출력하고, condition 옵션 값 'even' 과 함께 sort 옵션 값을 'desc'로 주면 짝수만 내림차순으로 정렬하여 출력한다.
이때 옵션 정보를 담은 해시를 인수로 넘길 때 '{' 와 '}' 이 없는 게 보이는데 해시가 가장 마지막 인수일 때는 '{' 와 '}' 를 생략할 수 있다.
해시를 사용해 메서드 호출 시 옵션을 전달하는 방법을 보았으니 이제 print_table 메서드에 색상 옵션 기능을 추가해 보자.
옵션 해시는 { color: {열인덱스 => 색깔심볼 또는 Proc객체} } 이러한 형태로 받는다고 생각하고 코드를 작성하였고 아래 전체 코드가 있다.
require 'rainbow'
module Utils
def self.print_table(data, options = {})
color_h = {}
if options[:color]
data[1..-1].each_with_index do |row, row_i|
options[:color].each do |col_i, color|
color_h[[row_i + 1, col_i + 1]] = color.is_a?(Proc) ? color.call(row[col_i]) : color
end
end
end
data = data.map do |row|
row.map do |col|
if col.is_a?(String)
col.encode("CP949")
else
col.to_s
end
end
end
data.each_with_index do |row, i|
if i == 0
row.unshift("#NO")
else
row.unshift(i.to_s)
end
end
col_widths = data.transpose.map { |cols| cols.map { |col| col.bytesize }.max }
c_del = " | "
l_del = "| "
r_del = " |"
data = data.map do |row|
row.map.with_index { |col, i| col.ljust(col_widths[i] + (col.size - col.bytesize)) }
end
color_h.each do |idx_pair, color|
row_i, col_i = idx_pair
col = data[row_i][col_i]
data[row_i][col_i] = Rainbow(col).__send__(color)
end
data = data.map { |row| "#{l_del}#{row.join(c_del)}#{r_del}" }
tot_width = col_widths.sum + (c_del.length * (col_widths.size - 1)) + l_del.length + r_del.length
line = "-" * tot_width
puts line
data.each_with_index do |str, i|
puts str
puts line if i == 0
end
puts line
end
end
우선 옵션은 color 옵션 하나만 있고 color 옵션의 값은 다시 해시로 표현되는데 색깔을 입힐 열의 인덱스를 키로 하고 색상 정보를 값으로 하는 해시이다.
색상 정보는 :red 또는 'red' 처럼 심볼이나 문자열로 줘도 되고 아니면 색깔을 결정하는 Proc 객체일 수 있다.
Proc 객체는 인수를 받아 특정 코드를 실행하고 결과를 반환할 수 있는 실행 가능한 객체라고 생각하면 된다.
즉, 특정 열의 값에 따라 색깔을 다르게 하고 싶으면 Proc 객체를 사용하면 된다.
print_table 메서드 안에서 data의 값에 대해 인코딩 또는 문자열 변환 작업을 하는데, 그 작업 보다 특정 위치의 값에 대한 최종 색깔을 결정하는 작업을 먼저 해야 한다.
print_table 메서드를 호출할 때 옵션에 사용할 Proc 객체의 코드는 print_table 메서드에 전달하는 data 가 갖는 원래의 값에 맞춰 작성했을텐데 변경된 값으로 Proc 객체의 코드가 실행된다면 원하는 색깔을 결과로 받지 못할 수 있기 때문이다.
특정 위치의 값에 대한 색깔 정보는 color_h 해시에 담기게 되는데 [행 인덱스, 열 인덱스] 배열이 키가 되고 색깔을 나타내는 심볼 또는 문자열이 값이 된다.
여기서 행 인덱스와 열 인덱스는 헤더와 행 번호 열을 생각해서 1씩 더해준 값이 들어 있다.
그 다음 할 일은 color_h 를 사용하여 해당 위치의 값에 실제 색깔을 입혀야 하는데 이 작업은 먼저 ljust 메서드를 통해 너비를 맞추고 난 후가 적당하다.
그렇지 않으면 원하는 대로 정렬이 되지 않는데 이유는 rainbow gem이 색깔을 입히기 위해 문자열을 ANSI 이스케이프 코드로 감싸기 때문이다. 즉, 문자열의 bytesize 값이 달라진다. 이에 대한 설명은 이 글 마지막에 조금 더 하겠다.
색깔을 입히는 부분에 __send__ 메서드가 보이는데 이 메서드는 대상 객체에 대해 호출하고 싶은 메서드를 그 메서드의 이름을 인수로 전달하여 호출할 수 있게 해준다.
rainbow gem 에서 색깔을 입힐 때 사용하는 메서드의 이름 자체가 해당 색깔을 나타내고 color_h 에 들어 있는 값도 색깔을 나타내는 심볼 또는 문자열이므로 __send__ 메서드를 통해 해당 색깔의 메서드를 호출할 수 있다.
즉 아래처럼 if/else 구문을 사용할 필요가 없다.
if color == :red
col = Rainbow(col).red
elsif color == :yellow
col = Rainbow(col).yellow
elsif color == :blue
col = Rainbow(col).blue
...
end
이제 테스트를 위해 아래 코드를 D:/blog/ruby/string_just 폴더에 test_option.rb 파일로 저장한 후 실행해 보자.
$LOAD_PATH.unshift("D:/blog/ruby/common_lib")
require 'utils/print_table'
data = [["종목명", "구매단가", "현재가", "수익률(%)"],
["삼성전자", "77,500", "74,300", -4.1],
["현대차", "245,000", "246,000", 0.4]]
color_proc = lambda { |v| v < 0 ? "blue" : "red" }
Utils.print_table(data, color: { 3 => color_proc })
data 배열에 수익률 정보를 추가하였고 Proc 객체를 통해 수익률이 0 미만이면 파란색 그렇지 않으면 빨간색으로 표시되도록 했다. color 옵션으로 전달한 해시에 열 인덱스를 3으로 지정했는데, 인덱스는 0부터 시작이고 print_table 메서드에 전달되는 데이터를 기준으로 '종목명' 열의 인덱스가 0이므로 '수익률(%)' 열의 인덱스는 3이 맞다.
현재는 색상 옵션만 있지만, 행 번호 표시 여부나 특정 열의 정렬 방식을 좌/우/가운데 중 하나로 선택할 수 있는 옵션을 여러분이 직접 추가해 보면 좋을 것 같다.
끝으로 위에서 사용한 Proc 과 rainbow gem 에 대해 조금 더 설명하고 이 글을 마무리하겠다.
먼저 Proc 객체를 여러 가지 방법으로 생성하는 아래 예제 그림을 보자.
Proc 객체는 lambda 메서드에 블록을 전달하여 생성할 수도 있고 Proc 클래스의 생성자를 통해서도 직접 생성이 가능하다.
또한 make_proc 메서드를 보면 메서드에 전달한 블록을 파라미터로 받으면, 결국 블록이 Proc 객체로 변환되어 파라미터에 전달됨을 알 수 있다.
이전에 Menu 클래스를 만들 때 메뉴에 대한 처리 코드를 담은 블록을 인스턴스 변수에 저장해 두었다가 메뉴 선택시 호출되도록 코드를 작성했었다.
'스피드 연산 게임 만들기' 시리즈의 8번째 글을 보면 해당 내용을 확인할 수 있다.
그런데 앞의 예제를 자세히 보면 뭔가 이상한 게 보이는데, 유독 lambda 메서드를 사용해서 생성한 Proc 객체의 호출 코드만 하나뿐이다.
다음 그림을 보면 알겠지만 Proc 객체를 생성할 때 lambda 메서드를 사용하게 되면 호출 시 인수의 개수를 엄격하게 따진다.
Proc 객체의 parameters 메서드는 호출 시 전달해야 할 파라미터에 대한 정보를 알려주는데, 아래 그림을 보면 Proc 클래스의 생성자와 make_proc 메서드를 사용해 생성한 Proc 객체에서는
파라미터 정보에 :opt(Optional)라고 나오지만 lambda 메서드를 사용해 생서한 Proc 객체에서는 파라미터 정보에 :req(Required)라고 나온다.
그런데 파라미터를 선언할 때 기본값을 설정할 수 있고 이때는 lambda 메서드를 사용한 Proc 객체의 파라미터 정보를 보면 해당 파라미터에 대해 :opt라고 나온다.
그리고 가변 인수를 받도록 파라미터를 선언해 놓으면 해당 파라미터에 대해서는 :rest 라고 나온다.
아래 그림을 보면 "\e[33mRuby\e[0m" 이렇게 출력되는 게 보이는데 가운데 원래의 문자열 'Ruby' 가 보인다.
즉, 위에서 얘기한 대로 rainbow gem의 색깔 메서드(yellow)가 원래의 문자열을 특정 색깔(여기선 노란색)을 나타내는 ANSI 이스케이프 코드로 감싼 것이다.
bytesize 값도 4가 아니라 13으로 나오는 것도 확인할 수 있다. 그래서 print_table 메서드에서 색깔을 입히기 전에 너비를 먼저 맞췄던 것이다.
그리고 "\e[33mJava\e[0m" 처럼 원하는 문자열을 ANSI 이스케이프 코드로 직접 감싸도 해당하는 색깔로 잘 출력됨을 알 수 있다.
즉 rainbow gem은 명확한 이름의 메서드(red, yellow 등등)를 통해 쉽게 원하는 색깔을 표현할 수 있도록 rainbow gem 소스 내에서 ANSI 이스케이프 코드와 관련된 복잡한 처리를 대신 해주고 있다고 보면 된다.
이것으로 '데이터 정렬해서 출력하기' 시리즈 글을 마치고, 다음 글에서는 rainbow gem이 제공하는 기능 중 아주 일부를 간단하게 만들어 볼까 한다.
See you agian~~