오늘은 특정 디렉터리 안에 포함된 항목들을 표시할 때 파일의 수정 날짜 및 파일 크기 등의 정보를 함께 표시해 주는 기능을 만들어 보려고 한다.
그런데 원하는 형태로 항목을 표시해 주는 기능을 FileSelector 클래스 안에 직접 구현하지 말고, 해당 기능을 전담으로 담당할 객체에게 작업을 맡기도록 프로그램을 작성해 보자.
현재 FileSelector 클래스에서 디렉터리의 항목을 표시해 주는 역할은 print_list 메서드가 하고 있는데, 이 기능 전체를 다른 객체에게 위임하기 보다는 기본 형식과 정보는 print_list 메서드가 제공하고 항목에 대한 추가적인 정보을 얻는 것만 다른 객체에게 위임하는 것이 좋을 것 같다.
그러면 먼저 아래 코드처럼 FileSelector 클래스의 코드를 변경해 보자.
class FileSelector
def initialize(options = {})
@dir = File.expand_path(options[:dir] || Dir.pwd)
@filter = options[:filter] || "*"
@only_file = options[:only_file]
@no_parent = options[:no_parent]
@file_info = options[:file_info]
end
...생략
def print_list(files)
width = files.size.to_s.length
files.each_with_index do |f, i|
no = (i + 1).to_s.rjust(width)
type = File.file?(f) ? "F" : "D"
info = @file_info.get_info(f) if @file_info && f != ".."
puts "#{no}. [#{type}] #{f} #{info}"
end
end
...생략
end
먼저 initialize 메서드에서는 옵션 해시에서 'file_info' 키의 값을 가져와 @file_info 인스턴스 변수에 할당한다. 추가 정보가 없는 게 기본이므로 별다른 기본값 설정은 필요 없다.
그리고 print_list 메서드는 @file_info에 위임할 객체가 할당되어 있고 현재 보여줄 항목이 상위 경로(..)가 아니라면 @file_info 객체에 대해 get_info 메서드를 호출하여 추가적인 정보를 가져와 함께 표시해 주도록 수정했다.
FileSelector 클래스에서 다른 코드는 변경된 게 없으므로 생략하였다.
다음으로 파일에 대한 추가적인 정보를 제공할 클래스를 만들어 보자.
아래 FileInfo 클래스의 코드를 보면 get_info라는 메서드에서 인수로 받은 값이 파일을 나타내는 경로일 경우 time 메서드와 size 메서드를 호출하여 얻은 정보로 문자열을 구성하여 반환하고 있다.
time 메서드는 File 클래스의 mtime 메서드를 사용하여 파일의 마지막 수정 시간(Time 객체)을 가져온 후 특정 날짜 시간 포맷을 적용하여 문자열로 변환해서 돌려주고, size 메서드는 File 클래스의 size 메서드를 사용하여 파일의 크기(바이트 단위)를 돌려준다.
class FileInfo
def get_info(file)
if File.file?(file)
"#{time(file)} #{size(file)}"
end
end
private
def time(file)
File.mtime(file).strftime("%Y-%m-%d %H:%M:%S")
end
def size(file)
File.size(file)
end
end
이제 FileInfo 클래스의 객체를 옵션으로 지정하여 FileSelector 클래스를 테스트해 보자.
D:/blog/ruby/file_selector 경로에 FileInfo 클래스의 코드를 file_info.rb 파일에 저장하고 FileSelector 클래스에서 변경된 부분도 file_selector.rb 파일에 반영하자.
그리고 해당 폴더에서 irb를 실행하여 아래 그림처럼 코드를 입력하고 결과를 확인해 보자.
원하는 테스트를 위해 FileSelector 객체를 생성할 때 FileInfo 객체를 사용하여 'file_info' 옵션을 지정해야 한다.
결과를 보면 정렬이 좀 필요해 보인다. 우선 FileSelector 클래스의 print_list 메서드에서 보여 주는 항목 이름에 대한 정렬부터 시작하자.
정렬을 위해서는 항목의 이름들 중에서 최대 너비를 구해야 하는데 항목 이름에 한글이 포함되어 있을 수 있으므로 먼저 인코딩을 'cp949'로 변경한 후에 각 항목 명의 bytesize 값들 중 최댓값을 구하자.
그리고 출력 전에 각 항목 이름을 ljust 메서드를 사용하여 최대 너비에 맞춰 왼쪽 정렬을 해 주었다.
문자열 정렬과 관련해서는 '데이터 정렬해서 출력하기' 글을 보면 자세한 설명이 나와 있으니 참고하길 바란다.
class FileSelector
...생략
def print_list(files)
no_width = files.size.to_s.length
files = files.map { |f| f.encode("cp949") }
f_width = files.map(&:bytesize).max
files.each_with_index do |f, i|
no = (i + 1).to_s.rjust(no_width)
type = File.file?(f) ? "F" : "D"
name = f.ljust(f_width + (f.size - f.bytesize))
info = @file_info.get_info(f) if @file_info && f != ".."
puts "#{no}. [#{type}] #{name} #{info}"
end
end
...생략
end
이어서 파일의 크기를 표시해 줄 때 정렬을 해주려면 print_list 메서드에서 @file_info 객체의 get_info 메서드를 호출할 때 정렬을 위한 최대 너비도 같이 넘겨줘야 한다.
그런데, 위임을 하는 객체와 위임을 받는 객체 간에 정해진 규칙(메서드 이름, 파라미터, 반환 값)을 변경하면서까지 특정 객체만을 위해 파라미터를 추가하는 것은 그리 좋은 생각이 아니다.
그래서 여기서는 파일의 크기를 표시할 때 너비를 10으로 어느 정도 여유 있게 주어 정렬하려고 한다.
그리고 파일의 크기를 표시할 때 세 자리마다 콤마도 함께 표시해 주자.
아래 add_comma 메서드는 인수가 양의 정수(자연수)를 나타내는 문자열이라고 가정하고 작성한 것이다. 4~6 자리 자연수면 콤마가 하나(1,234) 필요하고 7~9 자리면 두 개(1,234,567)가 그리고 10~12 자리면 세 개(1,234,567,890)가 필요하다.
즉 필요한 콤마의 개수는 전체 숫자 길이에서 1을 빼고 3으로 나눈 몫이 된다. 그리고 콤마를 삽입할 위치(인덱스)는 뒤에서부터 4 번째, 8 번째 등이 되므로 insert 메서드의 첫 번째 인수에 -4 * n 을 넣어줬다.
class FileInfo
def get_info(file)
if File.file?(file)
"#{time(file)} #{size(file)}"
end
end
private
def time(file)
File.mtime(file).strftime("%Y-%m-%d %H:%M:%S")
end
def size(file)
add_comma(File.size(file).to_s).rjust(10)
end
def add_comma(num)
comma_num = (num.length - 1) / 3
1.upto(comma_num) { |n| num.insert(-4 * n, ",") }
num
end
end
이제 변경한 내용을 해당 파일에 반영하고 아래 그림처럼 테스트를 진행해 보자.
항목들이 보기 좋게 정렬되어 출력되는 것을 볼 수 있다.
끝으로 항목이 디렉터리일 경우에는 마지막 수정 날짜와 더불어 해당 디렉터리 안에 포함된 디렉터리와 파일의 수를 함께 표시해 주도록 FileInfo 클래스를 수정해 보자.
아래 count 메서드를 보면 Dir 클래스의 glob 메서드를 사용하여 디렉터리 안의 모든 항목을 가져온 후 each 메서드를 통해 각 항목에 대해 블록 안에서 파일 여부를 검사하여 파일과 디렉터리의 수를 집계하고 있다.
그리고 if/else 문에 then을 사용하여 한 라인으로 작성해 보았다.
class FileInfo
def get_info(file)
if File.file?(file)
"#{time(file)} #{size(file)}"
else
"#{time(file)} #{count(file)}"
end
end
private
def time(file)
File.mtime(file).strftime("%Y-%m-%d %H:%M:%S")
end
def size(file)
add_comma(File.size(file).to_s).rjust(10)
end
def count(dir)
dir_cnt, f_cnt = 0, 0
Dir.glob("#{dir}/*").each { |f| if File.file?(f) then f_cnt += 1 else dir_cnt += 1 end }
"(D: #{add_comma(dir_cnt.to_s)}, F: #{add_comma(f_cnt.to_s)})"
end
def add_comma(num)
comma_num = (num.length - 1) / 3
1.upto(comma_num) { |n| num.insert(-4 * n, ",") }
num
end
end
이제 수정한 FileInfo 클래스가 잘 동작하는지 테스트를 진행해 보자.
D:/blog/ruby/file_selector 폴더의 file_info.rb 파일에 변경한 코드를 반영하고 해당 위치에서 irb를 실행한 후 아래 그림처럼 코드를 입력하여 결과를 확인해 보자.
항목이 디렉터리일 경우 포함하고 있는 디렉터리와 파일의 개수를 잘 표시해 주는 걸 볼 수 있다.
이것으로 '파일 선택기 만들기' 글을 마치도록 하겠다. 파일 크기를 KB 단위로 표시하는 등 여러분이 필요한 기능을 직접 추가해 보면 좋을 것 같다.
See you again~~