Адресная книга [Python etc]
Итак, задачка. Создать программу "Адресная книга", которая будет запоминать и выводить адреса и телефоны. Информация об объекте будет представлена в виде: ник, email, телефон. В программе должен быть поиск по нику, адресу, телефону, и по части ника, телефона и адреса. В последнем случае, должен быть выведен список из найденных абонентов.
Данные должны храниться в текстовом файле (для простоты) в произвольном виде (по выбору программиста).
Задачу решать на Python или других языках, о которых говорится тут. Сам попытаюсь решать на Python.
Условия для определения качества работы: краткость, работоспособность. Ну и читаемость. Кто создаст более краткий работающий код - тот и крут :)
(defun file-io-op (filename mode &optional data)
(with-open-file
(stream filename :direction mode :if-exists :supersede)
(if (eq mode :output)
(mapc #'(lambda (dp) (print dp stream)) data)
(loop when (read stream nil) collect it
else do (loop-finish)))))
;; ф-ция загрузки книги из файла
(defun book-load (filename)
(file-io-op filename :input))
;; ф-ция сохранения книги в файл
(defun book-save (book filename)
(file-io-op filename :output book))
;; обобщенная ф-ция поиска данных
(defun book-find (subseq storage &key storage-type)
(if (eq storage-type :file)
(setf storage (file-io-op storage :input)))
(mapcan
#'(lambda (sl)
(if (search (coerce subseq 'list)
(apply 'append
(mapcar #'(lambda (el) (coerce el 'list)) sl)))
(list sl) nil))
storage))
Конечно, очень даже не коротко, но это стартовый вариант, немного намеренно прямолинейный - с целью пошагового морфизма в кое-что более интересное. Зато работает - и не только для ника, адреса, телефона, но для любых данных в любом количестве.
[COLOR="SeaGreen"]Использование:[/COLOR]
[COLOR="RoyalBlue"]- входной файл состоит из записей вида ("ник" "мыло" "телефон" ...), например:[/COLOR]
[COLOR="Teal"]("Ander Skirnir" "some-email@mail.ru" "991-19-09")
("Evil Invader" "abigor@hellfire.pwnz" "1010011010")[/COLOR]
[COLOR="RoyalBlue"]- загрузка данных осуществляется функцией book-load:[/COLOR]
[COLOR="Teal"](defvar *adress-book* (book-load "input"))[/COLOR] [COLOR="Gray"];; вычитывает данные из файла input и записывает их в переменную *adress-book*[/COLOR]
[COLOR="RoyalBlue"]- запись данных в файл реализуется функцией book-save:[/COLOR]
[COLOR="Teal"](book-save *adress-book* "input")[/COLOR] [COLOR="Gray"];; записывает данные из переменной *adress-book* в файл input[/COLOR]
[COLOR="RoyalBlue"]- за поиск отвечает функция book-find, ищущая данные содержащие поданую на вход строчную последовательность:[/COLOR]
[COLOR="Teal"](book-find "rnir" *adress-book*)[/COLOR] [COLOR="Gray"]>> (("Ander Skirnir" "some-email@mail.ru" "991-19-09"))[/COLOR]
[COLOR="Teal"](book-find "@" *adress-book*)[/COLOR] [COLOR="Gray"]>> (("Ander Skirnir" "some-email@mail.ru" "991-19-09") ("Evil Invader" "abigor@hellfire.pwnz" "1010011010"))[/COLOR]
[COLOR="RoyalBlue"], также ей на вход можно кинуть не переменную, а имя файла, но тогда нужно указать дополнительный параметр: [/COLOR][COLOR="Teal"](book-find "@" "input" :storage-type :file)[/COLOR]
[COLOR="RoyalBlue"]- ну и да - добавление данных осуществляется стандартными лисповскими функциями:[/COLOR]
[COLOR="Teal"](push '("Yoda" "h@x.glctx" "Lli98-22131" "^.!.^") *adress-book*)[/COLOR][COLOR="Gray"] ;; и другими, например nconc и тп...[/COLOR]
Конечно, не против.
Однако, насколько я понял, ты сделал набор функций (может быть модуль), который может использовать в своей программе человек, который знает LISP. Или при интерактивной работе с интерпретатором.
Может быть условиям это и не противоречит, но подразумевалась какая-то прога, с которой смогут работать просто опытные пользователи.
В общем, я и сам немного застрял из-за того, что условия сформулированы слишком расплывчато. Постараюсь внести уточнения.
То что программа ищет по списку не разделяя на типы полей, это не плюс - это не верно. Понимаю что так в принципе сделать проще, но все же думаю стоит усложнить решения и придерживаться условий задачи -
То есть пользователь должен различать что именно он ищет - ник или адрес, и не получать по заданному нику кучу "левых" клиентов у которых совпадает с искомым емайл.
И второй момент.
Наверника ты знаком с книгой Practical Common Lisp, а именно глава 3. Практикум: Простая база данных (). Вообщем, как бы ты придумал и , возможно, реализовал интерфейс для доступа к адресной книге - просмотр, поиск и удаление, учитывая что пользователю совсем не обязательно знать Лисп что бы использовать твою программу.
Пользователь взаимодействует с программой при помощи определенного языка, который состоит из примерно следующего набора команд: help, find, edit, push, pop, quit.
Пользователь не должен знать, что есть файл, в котором хранится база. То есть, он может работать с базой без такого знания. База загружается и сохраняется без ведома пользователя. Так как пользователь может завершить работу программы не по команде quit, то сохранение базы должно производиться во время работы программы, а не при завершении ее.
Допускается пересохранять всю базу целиком при любом изменении, внесенном пользователем. Адресная Книга предназначена для одного пользователя, поэтому это не должно быть очень заметно.
Функции undo и redo не требуются на данном этапе. Хотя и не запрещается их реализовывать.
Описание функций:
help – выводит (основной) список команд. Если используется с параметром-командой, то выводится описание команды. Например
help find
выводит справку по команде find
Команду help реализовывать не обязательно – это факультативная фича.
find – имеет 2 параметра: поле, в котором надо искать и строку для поиска. Поле указывается с помощью слов nick, phone, email.
Примеры:
find nick andrew
find email www
find phone 3245533
При использовании этой команды программа должна выводить абонента, если требуемое поле полностью совпало, либо список из не более 10 абонентов, требуемое поле которых начинается с этой строки. Именно начинается – не надо усложнять пока. После можно какие-нибудь дополнительные правила ввести для поиска.
edit – команда для правки данных, использует один параметр – ник, который может быть опущен. В этом случае редактируются данные последнего найденного абонента (или первого в списке найденных). Подробности будут внесены при разработке.
push, pop – работают подобно edit. Используются для внесения или удаления абонента. Возможно объединение команд edit и push.
Надеюсь, не очень сложное задание.
Корректировки приветствуются.
Добавлено позже. Как справедливо мне заметили, это я писал для себя. Команды и логика работы может быть другая. Я только пример привел. Главное, чтобы пользователь не обязан был знать язык программирования, на котором написана программа и чтобы поиск был конкретным: по никам, по телефонам, по почте.
Команда edit которую ты предлагаешь без параметров для набора данных из 3-х полей в данной задаче не имеет смысла и зачем вводиться не ясно.
Почему упрощен поиск до совпадения с началом? Функцию поиска подстроки реализовать не сложно, да и тем более она почти везде есть.
Поэтому. Предлагаю.
Оставьте возможность придумать свою систему команд решающему. Это часть задачи, и причем одна из самых интересных.
Должен быть поиск (причем по нескольким полям), удаление (как 1 записи так и нескольких удовлетворяющих условию), добавление.
важна не сложность задачи, а смысл для пользователя. Если я хочу искать в никах, зачем мне искать везде.
Возможно.
Опять таки здесь я продумывал смысл для пользователя. Например, если в телефоне я набираю "к", то мне выдаются абоненты начинающиеся на букву "к", но не первые попавшиеся, в которых эта буква есть. Кто хочет может добавить хитрый синтаксис со звездочками и знаками вопроса, с регулярными выражениями и т.д.
Оставьте возможность придумать свою систему команд решающему. Это часть задачи, и причем одна из самых интересных.
Должен быть поиск (причем по нескольким полям), удаление (как 1 записи так и нескольких удовлетворяющих условию), добавление.
я только за. Я привел конкретный пример, чтобы было ясно о чем речь, как я вижу. Чтобы было о чем спорить :)
#-*-coding:cp1251-*-
import csv
import os
class View:
def OutAddress(self, nick, fields):
print nick, fields
def OutNames(self, names):
for name in names: print name
def OutMessage(self, mess):
print mess
def GetAddress(self):
phone = raw_input("Input a Phone: ")
email = raw_input("Input an Email: ")
return [phone, email]
def GetCommand(self):
return raw_input("Input command: ")
class Contr:
def __init__(self, view, book):
self.book = book
self.view = view
self.commands = {"quit": self.OnQuit, "q": self.OnQuit,
"names": self.OnNames, "find": self.OnFind, "f": self.OnFind,
"add": self.OnAdd, "del": self.OnDelete, "edit": self.OnEdit,
"help": self.OnHelp}
def Analysis(self, command):
if len(command):
parts = command.split(" ")
command = parts[0].strip()
if self.commands.has_key(command):
return self.commands[command](parts[1:])
else:
self.view.OutMessage("Use help for get info about commands")
def OnHelp(self, parts):
""" Help for help """
if len(parts):
if self.commands.has_key(parts[0]):
self.view.OutMessage(self.commands[parts[0]].__doc__)
else:
self.view.OutMessage("Not found: " + parts[0])
else: self.view.OutNames(sorted(self.commands.keys()))
def OnFind(self, parts):
""" Help for find """
if not len(parts): return
if self.book.has_key(parts[0]):
self.view.OutAddress(parts[0], self.book[parts[0]])
else:
nicks = self.book.keys()
notfound = True
for s in nicks:
name = str(s)
if name.find(parts[0]) == 0:
self.view.OutAddress(name, self.book[name])
notfound = False
if notfound: self.view.OutMessage(parts[0] + " not found")
def OnAdd(self, parts):
""" Help for add """
nick = parts[0]
self.book[nick] = self.view.GetAddress()
def OnDelete(self, parts):
""" Help for del """
nick = parts[0]
if self.book.has_key(nick):
self.book.pop(nick)
self.view.OutMessage(nick + " deleted")
else: self.view.OutMessage("nick not found!")
def OnEdit(self, parts):
""" Help for edit """
nick = parts[0]
if self.book.has_key(nick):
addr = self.view.GetAddress()
if(len(addr[0])):
self.book[nick][0] = addr[0]
if(len(addr[1])):
self.book[nick][1] = addr[1]
else: self.view.OutMessage("nick not found!")
def OnNames(self, parts):
""" Help for ... """
self.view.OutNames(self.book.keys())
def OnQuit(self, parts):
""" Help for quit """
return True
class Base:
nick, phone, email = 0, 1, 2
def __init__(self, fileName):
self.baseName = fileName
self.book = {}
def Load(self):
if os.path.exists(os.getcwd() + '\\' + self.baseName):
csvReader = csv.reader(open( self.baseName, "r" ), delimiter = ';')
for row in csvReader:
try: self.book[row[self.nick]] = [row[self.phone], row[self.email]]
except: pass
def Save(self):
csvWriter = csv.writer(open(self.baseName, "w" ), delimiter = ';')
for i in self.book:
csvWriter.writerow( + self.book)
class Dispatcher:
def __init__(self):
self.view = View()
self.base = Base("base.csv")
self.contr = Contr(self.view, self.base.book)
def Start(self):
self.view.OutMessage("Hi!")
self.base.Load()
def Cycle(self):
while True:
if self.contr.Analysis(self.view.GetCommand()): break
self.base.Save()
self.view.OutMessage("Bye!")
if __name__ == "__main__":
disp = Dispatcher()
disp.Start()
disp.Cycle()
Хочу сравнить реализацию некоторых приемов с реализацией их в C++.
1 Прием. Использование словаря (map, dict) с функциями. Данный прием позволяет ассоциировать команды пользователя с определенными функциями. В C++ пришлось бы заполнять std::map указателями на функцию, используя хитрый синтаксис. При том, все функции должны были быть одного типа. Для большей гибкости (то есть, чтобы еще приблизиться к тому, что есть в Python) нужно было бы создать иерархию классов (функторов) и заполнять map указателями на объекты данных классов.
2 Прием. Использование помощи. Я его не полностью осуществил, только основу сделал. Здесь для каждой функции можно предусмотреть комментарий, который будет использоваться как помощь по этой функции. Удобство заключается в том, что строка помощи располагается в самой функции. То есть, во-первых, ее можно использовать как комментарий, во вторых, всегда видно, есть ли у функции данная строка. В C++ опять пришлось бы создавать хитрую иерархию классов.
3 Прием. Не все функции в словаре одинаковы: одна функция возвращает True, остальные ничего не возвращают (честнее говоря, они возвращают None). Этот признак я использовал для определения того, когда требуется выйти из цикла требования команд от пользователя. В общем, это не явное преимущество, а скорее фича. В C++ наверно нужно было бы определить какое-то поле в иерархии функторов. Или в каждой функции возвращать False.
Что мне недоставало в Python (возможно, из-за плохого ознакомления с ним):
1. Не знаю, как сделать переменную (ссылку) константной, чтобы нельзя было ее случайно переопределить.
2. Нету нормальных enum. Такая удобная и очевидная штука как enum вроде бы должна быть в нормальном языке, но ее нет, например, и в Java (там есть некий странный одноименный класс, близкий по смыслу к словарю в Python).
Я не нашел пока подходящей альтернативы, поэтому кое-где у меня остались не вычищенные магические цифры и дублирование кода. То есть, в TODO остается избавление от дублирования и магических цифр, а также усовершенствование поиска.
import csv
import os
class Address: # module equivalent
phone = 0
email = 1
fields = ("a Phone", "an Email")
names = {"phone": phone, "email": email}
class View: # module equivalent
def OutAddress(nick, fields):
print nick, " => ", '; '.join([n + ": " + fields[Address.names[n]] for n in Address.names])
OutAddress = staticmethod(OutAddress)
def OutNames(names):
for name in names: print name
OutNames = staticmethod(OutNames)
def OutMessage(mess):
print mess
OutMessage = staticmethod(OutMessage)
def GetAddress():
return [raw_input("Input " + p + ": ") for p in Address.fields]
GetAddress = staticmethod(GetAddress)
def GetCommand():
return raw_input("Input command: ")
GetCommand = staticmethod(GetCommand)
class Base: # module equivalent
baseName = "base.csv"
def Load(book):
nick = 0
firstField = nick + 1
lastField = firstField + len(Address.fields)
if os.path.exists(os.getcwd() + '\\' + Base.baseName):
csvReader = csv.reader(open( Base.baseName, "r" ), delimiter = ';')
for row in csvReader:
try: book[row[nick]] = row[firstField : lastField]
except: pass
Load = staticmethod(Load)
def Save(book):
csvWriter = csv.writer(open(Base.baseName, "w" ), delimiter = ';')
for i in book:
csvWriter.writerow( + book)
Save = staticmethod(Save)
class Contr:
def __init__(self):
self.book = {}
Base.Load(self.book)
self.commands = {"quit": self.OnQuit, "q": self.OnQuit,
"names": self.OnNames, "find": self.OnFind, "f": self.OnFind,
"add": self.OnAdd, "del": self.OnDelete, "edit": self.OnEdit,
"help": self.OnHelp}
def Analysis(self, command):
if len(command):
parts = command.split(" ")
command = parts[0].strip()
if self.commands.has_key(command):
return self.commands[command](parts[1:])
else:
View.OutMessage("Use help for get info about commands")
def OnHelp(self, parts):
""" Help for help """
if len(parts):
if self.commands.has_key(parts[0]):
View.OutMessage(self.commands[parts[0]].__doc__)
else:
View.OutMessage("Not found: " + parts[0])
else: View.OutNames(sorted(self.commands.keys()))
def OnFind(self, parts):
""" Help for find """
if not len(parts): return
def FindNick(nick):
if self.book.has_key(nick):
View.OutAddress(nick, self.book[nick])
else:
nicks = self.book.keys()
notfound = True
for s in nicks:
name = str(s)
if name.find(nick) == 0:
View.OutAddress(name, self.book[name])
notfound = False
if notfound: View.OutMessage(nick + " not found")
def FindField(field, numb):
notfound = True
for addr in self.book:
fieldStr = str(self.book[addr][numb])
if fieldStr.find(field) == 0:
View.OutAddress(addr, self.book[addr])
notfound = False
if notfound: View.OutMessage(field + " not found")
if len(parts) == 1: FindNick(nick = parts[0])
elif len(parts) == 2:
name = parts[0]
if Address.names.has_key(name): FindField(parts[1], Address.names[name])
else: View.OutMessage("You have used invalid key: ", name)
def OnAdd(self, parts):
""" Help for add """
nick = parts[0]
self.book[nick] = View.GetAddress()
Base.Save(self.book)
def OnDelete(self, parts):
""" Help for del """
nick = parts[0]
if self.book.has_key(nick):
self.book.pop(nick)
Base.Save(self.book)
View.OutMessage(nick + " deleted")
else: View.OutMessage("nick not found!")
def OnEdit(self, parts):
""" Help for edit """
nick = parts[0]
if self.book.has_key(nick):
addr = View.GetAddress()
for i in xrange(len(addr)):
if(len(addr)): self.book[nick] = addr
Base.Save(self.book)
else: View.OutMessage("nick not found!")
def OnNames(self, parts):
""" Help for ... """
View.OutNames(self.book.keys())
def OnQuit(self, parts):
""" Help for quit """
return True
def Cycle():
View.OutMessage("Hi!")
contr = Contr()
while True:
if contr.Analysis(View.GetCommand()): break
View.OutMessage("Bye!")
if __name__ == "__main__":
Cycle()
Ну что же, на курсовую третьекурсника тянет :)
(:use :common-lisp :iterate))
(in-package :address-book)
;; обобщенная ф-ция чтения-записи в файл
(defun file-io-op (filename mode &optional data)
(with-open-file
(stream filename :direction mode :if-exists :supersede)
(if (eq mode :output)
(print data stream)
(read stream nil))))
;; загрузка базы из файла
(defvar *address-book* (file-io-op "input" :input))
(defvar *basis* (cadr (assoc 'basis *address-book*)))
;; функция помощи нуждающимся
(defun userfunc-help (&optional command)
(format t
(or (case command
((help) ">> prints command's description~%>> e.g: help find~%~%")
((find) ">> find records by fields and its values~%~%")
((nil) ">> command list:~%>- help~%>- find~%~%>? for extra info about command~%>? type help %cmdname%, for e.g:~%>? help find~%~%"))
">> invalid argument!~%~%")))
;; макрос высшего порядка
(defmacro adbook-high-order (names conditions code)
(let ((target-list (cadr (assoc 'data *address-book*))))
`(iter (for lst* :on ',target-list)
,@(mapcar #'(lambda (name)
`(for ,name := (nth ,(position name *basis*) (car lst*))))
(eval names))
(if ,(eval conditions) ,(eval code)))))
(defun userfunc-dump ()
(print *address-book*))
;; поиск
(defun userfunc-find (&rest request)
(let* ((lst (divide-by-tokens '(where) (if (equal (car request) 'record) (append *basis* (cdr request)) request)))
(names (delete 'and (car lst)))
(conditions (reduce 'infix-prefix `((and or) (equal ~) ,(nsubst 'equal '= (cadr lst))) :from-end t))
(code `(collect (list ,@names)))
(result (eval `(adbook-high-order ',names ',conditions ',code))))
(iter (for r :in result)
(iter (for i :in names)
(for c :in r)
(format t ">> ~a: ~a~%" i c))
(finally (format t "~%")))))
;; добавление
(defun userfunc-add-record (&rest request)
(let* ((lst (reduce 'delete `(and with = ,request) :from-end t))
(result (iter (for field :in *basis*)
(collect (car (iter (for value :on lst :by 'cddr)
(if (equal field (car value))
(collect (cadr value)))))))))
(if (notevery #'null result) (push result (cadadr *address-book*)))))
;; удаление
(defun userfunc-delete-record (&rest request)
(let* ((lst (delete 'with request))
(names (intersection lst *basis*))
(conditions (reduce 'infix-prefix `((and or) (equal ~) ,(nsubst 'equal '= lst)) :from-end t))
(code '(progn (format t ">> deleting record ~s~%" (car lst*)) (rplaca lst* (cadr lst*)) (rplacd lst* (cddr lst*)))))
(eval `(adbook-high-order ',names ',conditions ',code))))
;; функция сопоставления со строчным образцом
(defun ~ (var-a var-b)
(if (not (stringp var-b))
(setf var-b (format nil "~a" var-b)))
(if (equal (elt var-b (1- (length var-b))) #\*)
(if (= (or (search (string-right-trim "*" var-b) var-a) -1) 0) t nil)
(if (equal (elt var-b 0) #\*)
(if (> (or (search (string-left-trim "*" var-b) var-a) -1) 0) t nil)
(if (search var-b var-a) t nil))))
;; ф-ция перевода инфиксной записи в лисповаримую префиксную скобочную
(defun infix-prefix (operator-list target-list)
(let* ((lst* (copy-list target-list))
(result
(iter (for i := lst*) (until (null lst*))
(if (member (cadr i) operator-list)
(progn (collect (cons (cadr i)
(mapcar #'(lambda (k)
(if (listp k) (infix-prefix operator-list k) k))
(list (car i) (caddr i)))))
(setf lst* (cdddr lst*)))
(progn (collect (car i)) (setf lst* (cdr lst*)))))))
(if (listp (car result)) (car result) result)))
;; ф-ция разбиения списка на подсписки токенами
(defun divide-by-tokens (token-list target-list)
(iter (for i :in target-list)
(if (member i token-list)
(progn (collect temp :into result)
(setf temp '()))
(collect i :into temp))
(finally (return (nconc result (list temp))))))
;; парсит строку в лисповскую форму,
;; голова цела, хвост - заквочен
(defun adbook-parse-form (text-form)
(let ((form (read (make-string-input-stream text-form))))
(let ((tild* (if (member '~ form) (nthcdr (1+ (position '~ form)) form))))
(if tild* (rplaca tild* (format nil "~a" (car tild*)))))
(cons (car form) (mapcar #'(lambda (s) `',s) (cdr form)))))
;; приветствие и приглашение
(format t "| Address Book Operating Terminal |~% ...(type help to get cmdlist)... ~%~%address-book > ")
;; mainloop
(iter (for query := (string-right-trim '(#\Return) (read-line)))
(until (string= "exit" query))
(let ((form (adbook-parse-form (concatenate 'string "(userfunc-" query ")"))))
(if (fboundp (car form))
(eval form)
(format t ">> invalid command!~%~%")))
(princ "address-book > ")
(force-output t))
Допустим имеются записи:
nick/email/phone :
("Ander Skirnir" "some-email@mail.ru" 991-19-09)
("Evil Invader" "abigor@hellfire.pwnz" 1010011010)
Комманды поддерживаются такого рода:
[COLOR="SeaGreen"]find nick where phone = 991-19-09[/COLOR]
>> Nick: "Ander Skirnir"
[COLOR="SeaGreen"]find nick and email where (nick ~ "der" and email ~ "@") or 1 = 1[/COLOR]
>> Nick: "Ander Skirnir"
>> Email: "some-email@mail.ru"
>> Nick: "Evil Invader"
>> Email: "abigor@hellfire.pwnz"
[COLOR="SeaGreen"]find record where nick ~ "A*"[/COLOR]
>> Nick: "Ander Skirnir"
>> Email: "some-email@mail.ru"
>> Phone: 991-19-09
ну и
[COLOR="SeaGreen"]add-record with nick = ...[/COLOR]
[COLOR="SeaGreen"]delete-record with phone ~ ...[/COLOR] и тп
Мораль оказалась в том, что не следует делать чёрт-знает-что вместо базы данных, если требуется её функциональность, в надежде, что это получится намного легче (я пытался всё сделать на списках :)
И дело не в том, что получилось сильно плохо, а в том, что на данном этапе уже могла быть простенькая бд.
В следующем варианте я попробую хоть немного приблизиться к lisp-way и написать dsl, решающую целый класс задач типа "адресная книга" - а это и есть бд.
P.S. Как только личная запарка кончится, кину вариант на Erlang'е. Думаю, смогу через пару дней.
Так у меня практически на C++ и написано. На "правильном" Питоне кодить еще не научился... А так переводится один в один в C++. Может и переведу как-нибудь. Но я думал, что вполне хватит сравнительного комментария (с C++), который я уже сделал.
Кстати, я сделал ООП-шную ошибку в своем последнем варианте. Все-таки надо было пользоваться объектом класса View - для перевода в GUI-шный вариант пригодилось бы.
Посмотрел на вариант от Ander Skirnir и понял, что теме надо сменить название на "Вивилонская башня" ))
using System.Collections.Generic;
using System.Console; // можно раскрывать статические члены класса :)
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml.Serialization;
using Nemerle;
using Nemerle.Assertions;
using Nemerle.Utility;
[Record] // макрос создаст конструктор, инициализирующий поля
[XmlType("Address")]
[Serializable]
public sealed class AddressEntry {
public this() { // требуется для работы сериализатора
}
[Accessor(flags = WantSetter)] // создает свойство с геттером и сэтером
private mutable nick : string = string.Empty;
[Accessor(flags = WantSetter)]
private mutable email : string = string.Empty;
[Accessor(flags = WantSetter)]
private mutable phone : string = string.Empty;
public override ToString() : string {
$"$nick, $email, $phone"
}
}
[XmlRoot] // корневой элемент XML файла
[XmlType("AddressBook")]
[Serializable]
public sealed class AddressEntryCollection : List[AddressEntry] {
}
// аналог перечисления enum
public enum SearchCriteria {
| Nick
| Phone
}
public sealed class AddressBook : IDisposable {
private static serializer : XmlSerializer = XmlSerializer(typeof(AddressEntryCollection));
public this(file_name : string = "AddressBook.xml") // можно использовать значения параметров по-умолчанию
requires !string.IsNullOrEmpty(file_name) // проверка параметра на корректность
otherwise throw ArgumentException("Parameter 'file_name' must have non-emtpy value.")
{
this.file_name = file_name;
this.data = if(File.Exists(file_name)) {
using(stream = FileStream(file_name, FileMode.Open, FileAccess.Read))
serializer.Deserialize(stream) :> AddressEntryCollection
} else
AddressEntryCollection()
}
[Accessor]
private file_name : string;
private data : AddressEntryCollection;
public Find(str : string, criteria : SearchCriteria) : list[AddressEntry] {
def condition = match(criteria) {
| Nick => fun(rec) { rec.Nick.IndexOf(str) >= 0 }
| Email => fun(rec) { rec.Email.IndexOf(str) >= 0 }
| Phone => fun(rec) { rec.Phone.IndexOf(str) >= 0 }
}
def searcher(seq) {
if (seq.MoveNext())
if (condition(seq.Current))
seq.Current :: searcher(seq)
else searcher(seq)
else []
}
using(seq = data.GetEnumerator()) {
searcher(seq)
}
}
public Add(rec : AddressEntry) : void {
unless(data.Contains(rec))
data.Add(rec)
}
public Remove(rec : AddressEntry) : void {
_ = data.Remove(rec)
}
protected override Finalize() : void { // "деструктор" в терминах C#
Dispose(false);
}
private Dispose(disposing : bool) : void {
ignore(disposing); // чтобы компилятор не ругался на неиспользуемый параметр
using(stream = FileStream(file_name, FileMode.Create, FileAccess.Write))
serializer.Serialize(stream, data)
}
private mutable disposed : bool = false;
public Dispose() : void {
unless(disposed) {
Dispose(true);
GC.SuppressFinalize(this); // отключаем "деструктор"
disposed = true;
}
}
}
module Program {
Main() : void {
WriteLine("Welcome to address book keeper!");
using(ab = AddressBook()) {
WriteLine("Address book now loaded.");
mutable quit = false;
while(!quit) {
def input = ReadLine();
when(input != string.Empty)
match(ParseInput(input)) {
| "q" :: [] | "quit" :: [] | "exit" :: [] => quit = true;
| "f" :: tail | "find" :: tail => DoFind(ab, tail);
| "a" :: tail | "add" :: tail => DoAdd(ab, tail);
| "e" :: tail | "edit" :: tail => DoEdit(ab, tail);
| "d" :: tail | "delete" :: tail => DoDelete(ab, tail);
| _ => WriteLine("Input not recognized :(");
}
}
WriteLine("Saving changes...");
}
WriteLine("Bye!");
}
private pattern : Regex = Regex(<#(\".*?\")|(=)|((\w|\.|@|_|-)+)#>, RegexOptions.Compiled);
ParseInput(str : string) : list[string] {
def list_builder(seq) {
if(seq.MoveNext()) {
def m = seq.Current :> Match;
if(m.Success)
m.Value :: list_builder(seq)
else list_builder(seq)
} else []
}
list_builder(pattern.Matches(str).GetEnumerator())
}
DoFind(ab : AddressBook, input : list[string]) : void {
def result = match(input) {
| "n" :: nick :: []
| "nick" :: nick :: [] =>
ab.Find(nick, SearchCriteria.Nick)
| "e" :: email :: []
| "email" :: email :: [] =>
ab.Find(email, SearchCriteria.Email)
| "p" :: phone :: []
| "phone" :: phone :: [] => ab.Find(phone, SearchCriteria.Phone)
| _ => WriteLine("Input for searching not recognized :(");
[]
}
match(result) {
| [] => WriteLine("No records found.");
| _ => WriteLine($"$(result.Length) record(s) found:");
result.Iter(WriteLine);
}
}
SetEntryField(entry : AddressEntry, field : string, value : string) : bool {
match(field) {
| "n" | "nick" => entry.Nick = value; true
| "e" | "mail" | "email" => entry.Email = value; true
| "p" | "phone" | "mobile" => entry.Phone = value; true
| _ => false
}
}
ParseEntryFields(entry : AddressEntry, input : list[string]) : bool {
match(input) {
| [] => true
| field :: "=" :: value :: tail
| field :: value :: tail =>
ParseEntryFields(entry, tail) && SetEntryField(entry, field, value)
| _ => false
}
}
DoAdd(ab : AddressBook, input : list[string]) : void {
def entry = AddressEntry();
if(ParseEntryFields(entry, input)) {
ab.Add(entry);
WriteLine($"New record added: $entry");
} else
WriteLine("Invalid input :(");
}
DoEdit(ab : AddressBook, input : list[string]) : void {
match(input) {
| nick :: tail =>
match(ab.Find(nick, SearchCriteria.Nick)) {
| entry :: _ =>
if(ParseEntryFields(entry, tail))
WriteLine($"Record changed to: $entry.")
else
WriteLine($"Some input not recognized, new record: $entry.")
| _ => WriteLine($"No records for '$nick' found.");
}
| _ => WriteLine("Invalid input :(");
}
}
DoDelete(ab : AddressBook, input : list[string]) : void {
match(input) {
| nick :: [] =>
match(ab.Find(nick, SearchCriteria.Nick)) {
| entry :: _ => {
WriteLine($"Deleting first found entry $entry");
ab.Remove(entry);
}
| _ => WriteLine($"No records for '$nick' found.");
}
| _ => WriteLine("Invalid input :(");
}
}
}
Добавление:
a n=hardcase e=hc@net.du p=9031112234
или вот так:
add nick hardcase email [EMAIL="hc@net.du"]hc@net.du[/EMAIL] phone 9031112234
или даже вот так:
add nick=hardcase email=hc@net.du phone=9031112234
Поиск:
find nick ha
Редактирование:
edit ha n=hardcaseminator e=hc@bla.bla.net
(изменит первую попавшуюся)
Удалить тоже можно
delete ha
(удалит первую запись)
Допустим имеются записи:
nick/email/phone :
("Ander Skirnir" "some-email@mail.ru" 991-19-09)
("Evil Invader" "abigor@hellfire.pwnz" 1010011010)
Комманды поддерживаются такого рода:
[COLOR="SeaGreen"]find nick where phone = 991-19-09[/COLOR]
>> Nick: "Ander Skirnir"
я Lisp не знаю - пытаюсь интуитивно понять. Вижу, что в коде нигде нет слов типа phone, email и т.п. Возможно, они хранятся в файле базы или в каком-то дополнительном файле настроек.
Если в файле базы, то программа не будет адекватно работать без такого файла, что предусмотрено в моей и Хардкейсовской версиях. С другой стороны, так добавляется некая гибкость для пользователя.
(data
(("Ander Skirnir" "some-email@mail.ru" 991-19-09)
("Evil Invader" "abigor@hellfire.pwnz" 1010011010))))
Т.е. в первом подсписке хранится базис, из которого находится координата нужной компоненты по её имени при работе с записями со второго подсписка.
Именно это мы с Когромом и предположили на #codenet. :)
Ну так. Нормальный lisp-way. На Эрланге будет похоже.
P.S. Передумал выкладывать простой вариант. mnesia прикручу ;)
Python / Ruby / Groovy и пр.
Haskell / Erlang / OCaml и т.д
Может быть, стоит перенести эту тему в какой-нибудь из них, а то потеряется со временем в "Студентах"...
Правда, непонятно куда переносить - здесь есть решения, как я понял, на языках из обоих разделов...