今天又重讀了 HuangZ 翻譯的 Practical Common Lisp 個人翻譯版。
看完以後試著拿 Ruby 實現一遍(除了宏以外)。
題目是簡單的資料庫,記錄唱片(CD)的資料,唱片有標題、作者、評分以及是否已經轉換成 MP3 的紀錄。
首先資料庫就是一個簡單的 Ruby 陣列,裡面每一筆便是一張唱片(Hash 形式保存):
$db = []
新增一張唱片:
def make_cd(title, artist, rating, ripped)
{ title: title, artist: artist, rating: rating, ripped: ripped }
end
把唱片資料保存到資料庫裡:
def add_record(record)
$db.push record
end
這麼保存不太人性化,加入一個互動式新增唱片的介面:
def prompt_read(prompt)
print "#{prompt}: "
return STDIN.readline.chomp
end
def prompt_for_cd
make_cd(
prompt_read('Title'),
prompt_read('Artist'),
prompt_read('Rating').to_i,
yes_or_no?('Ripped [y/n]')
)
end
prompt_read('Rating').to_i
我們希望評分是數字,而不是文字內容。
prompt_read
的功用是印出一個提示符,接著讀取輸入(去掉換行符)。
上面還用到一個方法是:yes_or_no?
,這是從 Common Lisp 學來的一個函數:y-or-n-p
。
輸入不是 y
、Y
、n
、N
開始的字元,便會重新要求輸入:
def yes_or_no?(input)
first_time = true
print "#{input}: "
while (input =~ /^(y|yes|n|no)/i).nil?
print 'Please answer y or n: ' if first_time != true
input = gets.chomp
first_time = false
end
input =~ /y/i ? true : false
end
有了 add_record
與 prompt_for_cd
之後,便可以一次新增多筆資料了:
def add_cds
loop do
add_record(prompt_for_cd)
break unless yes_or_no?("Another? [y/n]")
end
end
> add_cds
Title: 再見青春
Artist: 汪鋒
Rating: 9
Ripped [y/n]: n
Another? [y/n]: y
Title: 致青春
Artist: 王菲
Rating: 9999
Ripped [y/n]: n
Another? [y/n]: n
=> nil
現在我們已經可以成功的往資料庫裡面添加唱片資料了。
現在看看怎麼樣把資料庫裡的唱片資料印出來:
def dump_db
$db.each do |cds|
cds.each { |key, value|
printf(STDOUT, "%s \t\t\t %s\n", key, value)
}
end; nil # supress self return
end
> dump_db
title 再見青春
artist 汪鋒
rating 9
ripped false
title 致青春
artist 王菲
rating 9999
ripped false
接著看看怎麼樣把資料庫的資料儲存到電腦裡:
def save_db(filename)
require 'fileutils'
FileUtils.touch(filename) unless File.exist? filename
File.open(filename, 'w') { |f|
require 'yaml'
f.write($db.to_yaml)
}
end
save_db "cd.db"
就會存在當下目錄,存成一個 cd.db
檔案。
下次再載入也是很簡單:
def load_db(filename)
File.exist?(filename) && $db = YAML.load_file(filename)
end
load_db "cd.db"
這裡儲存的時候採用的是 YAML
格式,方便處理。
大致上的功能都搞定了。
接下來看怎麼從資料庫選出資料,比如按照標題選出我們要的唱片:
def select_by_title(title)
$db.select(->(cd) { cd[:title] == title })
end
於是便可以實現 select_by_artist
, select_by_rating
,再把 select
的介面統一一下:
def select selector
$db.select(&selector)
end
def select_by_title(title)
$db.select(->(cd) { cd[:title] == title })
end
def select_by_artist(artist)
$db.select(->(cd) { cd[:artist] == artist })
end
def select_by_rating(rating)
$db.select(->(cd) { cd[:rating] == rating })
end
def select_by_ripped(ripped)
$db.select(->(cd) { cd[:ripped] == ripped })
end
可以用 define_method
定義出這些方法,這裡保留講解的易懂性,就不貼出來了。
這樣還是不太爽,有沒有像是 SQL 的 SELECT ... WHERE ...
:
def where(title: nil, artist: nil, rating: nil, ripped: nil)
->(cd) {
title ? cd[:title] == title : true &&
artist ? cd[:artist] == artist : true &&
rating ? cd[:rating] == rating : true &&
ripped ? cd[:ripped] == ripped : true
}
end
現在便可以:
select(where(rating: 10))
這麼選。
更新也是差不多的思路:
def update(selector, title: nil, artist: nil, rating: nil, ripped: nil)
$db.each { |row|
if selector.call(row)
row[:title] = title if title
row[:artist] = artist if artist
row[:rating] = rating if rating
row[:ripped] = ripped if ripped
end
}
end
從資料庫裡刪除資料也很簡單:
def delete_rows(selector)
$db.delete_if(&selector)
end
(完)