diff --git a/.fasterer.yml b/.fasterer.yml new file mode 100644 index 0000000..5ba63b2 --- /dev/null +++ b/.fasterer.yml @@ -0,0 +1,21 @@ +speedups: + rescue_vs_respond_to: true + module_eval: true + shuffle_first_vs_sample: true + for_loop_vs_each: true + each_with_index_vs_while: false + map_flatten_vs_flat_map: true + reverse_each_vs_reverse_each: true + select_first_vs_detect: true + sort_vs_sort_by: true + fetch_with_argument_vs_block: true + keys_each_vs_each_key: true + hash_merge_bang_vs_hash_brackets: true + block_vs_symbol_to_proc: true + proc_call_vs_yield: true + gsub_vs_tr: true + select_last_vs_reverse_detect: true + getter_vs_attr_reader: true + setter_vs_attr_writer: true + +exclude_paths: diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..59ace56 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ + db.yml +*.kate-swp diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..41ac85e --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,11 @@ +AllCops: + NewCops: enable + +Style/RedundantArgument: # (new in 1.4) + Enabled: true +Style/SwapValues: # (new in 1.1) + Enabled: true +Style/FrozenStringLiteralComment: + Enabled: false +Style/Documentation: + Enabled: false diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 0000000..aedc15b --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +2.5.3 diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..82aa4d4 --- /dev/null +++ b/Gemfile @@ -0,0 +1,7 @@ +source 'https://rubygems.org' + +gem 'awesome_print' +gem 'faker' +gem 'fasterer' +gem 'pry-byebug' +gem 'rubocop', require: false diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..3ac4b0f --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,58 @@ +GEM + remote: https://rubygems.org/ + specs: + ast (2.4.1) + awesome_print (1.8.0) + byebug (11.1.3) + coderay (1.1.3) + colorize (0.8.1) + concurrent-ruby (1.1.7) + faker (2.15.1) + i18n (>= 1.6, < 2) + fasterer (0.8.3) + colorize (~> 0.7) + ruby_parser (>= 3.14.1) + i18n (1.8.5) + concurrent-ruby (~> 1.0) + method_source (1.0.0) + parallel (1.20.1) + parser (3.0.0.0) + ast (~> 2.4.1) + pry (0.13.1) + coderay (~> 1.1) + method_source (~> 1.0) + pry-byebug (3.9.0) + byebug (~> 11.0) + pry (~> 0.13.0) + rainbow (3.0.0) + regexp_parser (2.0.2) + rexml (3.2.4) + rubocop (1.7.0) + parallel (~> 1.10) + parser (>= 2.7.1.5) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 1.8, < 3.0) + rexml + rubocop-ast (>= 1.2.0, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 1.4.0, < 2.0) + rubocop-ast (1.3.0) + parser (>= 2.7.1.5) + ruby-progressbar (1.10.1) + ruby_parser (3.15.0) + sexp_processor (~> 4.9) + sexp_processor (4.15.1) + unicode-display_width (1.7.0) + +PLATFORMS + ruby + +DEPENDENCIES + awesome_print + faker + fasterer + pry-byebug + rubocop + +BUNDLED WITH + 2.0.1 diff --git a/autoload.rb b/autoload.rb new file mode 100644 index 0000000..1027840 --- /dev/null +++ b/autoload.rb @@ -0,0 +1,12 @@ +require 'date' +require 'yaml' +require 'faker' +require_relative 'helpers/error_class' +require_relative 'helpers/seed' +require_relative 'modules/validation' +require_relative 'modules/statistics' +require_relative 'models/author' +require_relative 'models/reader' +require_relative 'models/book' +require_relative 'models/order' +require_relative 'models/library' diff --git a/helpers/error_class.rb b/helpers/error_class.rb new file mode 100644 index 0000000..2327193 --- /dev/null +++ b/helpers/error_class.rb @@ -0,0 +1,14 @@ +class PresenceError < StandardError +end + +class ClassError < StandardError +end + +class EmptinessError < StandardError +end + +class PositiveNumberError < StandardError +end + +class NotAllowedObject < StandardError +end diff --git a/helpers/seed.rb b/helpers/seed.rb new file mode 100644 index 0000000..f5333e7 --- /dev/null +++ b/helpers/seed.rb @@ -0,0 +1,46 @@ +class Seed + attr_reader :library + + def initialize(library) + @library = library + end + + def generate_data + seed_authors + seed_books + seed_readers + seed_orders + end + + def seed_authors + 10.times do + library.add(Author.new(name: Faker::Book.unique.author, + biography: Faker::Lorem.sentence(word_count: 10))) + end + end + + def seed_books + 50.times do + library.add(Book.new(title: Faker::Book.unique.title, + author: library.authors.sample)) + end + end + + def seed_readers + 20.times do + library.add(Reader.new(name: Faker::Name.unique.name, + email: Faker::Internet.unique.email, + city: Faker::Address.city, + street: Faker::Address.street_name, + house: Faker::Address.building_number.to_i)) + end + end + + def seed_orders + 100.times do + library.add(Order.new(book: library.books.sample, + reader: library.readers.sample, + date: Date.today - rand(0..30))) + end + end +end diff --git a/main.rb b/main.rb new file mode 100644 index 0000000..a1663df --- /dev/null +++ b/main.rb @@ -0,0 +1,47 @@ +require_relative 'autoload' + +reader1 = Reader.new(name: 22, email: '@sabaka', street: '', house: -10) +reader2 = Reader.new(name: 'Mark', email: 'markus@gmail.com', city: 'London', street: 'Bayker', house: 21) +author1 = Author.new(name: 'Boris Akunin') +author2 = Author.new(name: '', biography: 'some bio') +book1 = Book.new(title: 'BEloe solnce', author: author1) +book2 = Book.new(title: '', author: 'Djek Vorobey') +order1 = Order.new(book: book2, reader: reader1) +order2 = Order.new(book: book1, reader: reader2) + +lib = Library.new +lib.add(reader2) +lib.add(author1) +lib.add(book1) +lib.add(order2) + +begin + lib.add(author2) +rescue EmptinessError => e + puts e.message +end + +begin + lib.add(reader1) +rescue PresenceError => e + puts e.message +end + +begin + lib.add(book2) +rescue ClassError => e + puts e.message +end + +begin + lib.add(order1) +rescue ClassError => e + puts e.message +end + +Seed.new(lib).generate_data +lib.store_to_file + +puts lib.top_readers(lib.orders) +puts lib.top_books(lib.orders) +puts lib.number_readers_of_top_books(lib.orders) diff --git a/models/author.rb b/models/author.rb new file mode 100644 index 0000000..63124ed --- /dev/null +++ b/models/author.rb @@ -0,0 +1,33 @@ +class Author + include Validator + + attr_reader :name, :biography + + def initialize(name: nil, biography: nil) + @name = name + @biography = biography + end + + def validate! + check_presence + check_class + check_emptiness + end + + private + + def check_presence + errors = validate_presence :name + raise PresenceError, errors unless errors.empty? + end + + def check_class + errors = validate_class String, :name + raise ClassError, errors unless errors.empty? + end + + def check_emptiness + errors = validate_emptiness :name + raise EmptinessError, errors unless errors.empty? + end +end diff --git a/models/book.rb b/models/book.rb new file mode 100644 index 0000000..263dbaa --- /dev/null +++ b/models/book.rb @@ -0,0 +1,35 @@ +class Book + include Validator + + attr_reader :title, :author + + def initialize(title: nil, author: nil) + @title = title + @author = author + end + + def validate! + check_presence + check_class + check_emptiness + @author.validate! + end + + private + + def check_presence + errors = validate_presence :title, :author + raise PresenceError, errors unless errors.empty? + end + + def check_class + errors = [validate_class(String, :title), + validate_class(Author, :author)].flatten + raise ClassError, errors unless errors.empty? + end + + def check_emptiness + errors = validate_emptiness :title + raise EmptinessError, errors unless errors.empty? + end +end diff --git a/models/library.rb b/models/library.rb new file mode 100644 index 0000000..7e39cb8 --- /dev/null +++ b/models/library.rb @@ -0,0 +1,45 @@ +class Library + include Statistics + + DATA_FILE = File.expand_path('../db.yml', __dir__) + CLASSES = [Integer, String, Date, Author, Book, Library, Order, Reader].freeze + ADDING_ERROR = "Object cann't be added to library".freeze + + attr_reader :authors, :books, :readers, :orders + + def initialize + @authors = [] + @books = [] + @readers = [] + @orders = [] + + load_from_file! + end + + def add(entity) + entity.validate! if entity.respond_to?(:validate!) + case entity + when Author then @authors << entity + when Book then @books << entity + when Reader then @readers << entity + when Order then @orders << entity + else + raise NotAllowedObject, ADDING_ERROR + end + end + + def load_from_file! + data = YAML.safe_load(File.read(DATA_FILE), CLASSES, [], true) if File.exist?(DATA_FILE) + + return unless data + + @authors = data.authors + @books = data.books + @readers = data.readers + @orders = data.orders + end + + def store_to_file + File.open(DATA_FILE, 'w') { |file| file.write(to_yaml) } + end +end diff --git a/models/order.rb b/models/order.rb new file mode 100644 index 0000000..c6d8076 --- /dev/null +++ b/models/order.rb @@ -0,0 +1,32 @@ +class Order + include Validator + + attr_reader :book, :reader, :date + + def initialize(book: nil, reader: nil, date: Date.today) + @book = book + @reader = reader + @date = date + end + + def validate! + check_presence + check_class + @book.validate! + @reader.validate! + end + + private + + def check_presence + errors = validate_presence :book + raise PresenceError, errors unless errors.empty? + end + + def check_class + errors = [validate_class(Book, :book), + validate_class(Reader, :reader), + validate_class(Date, :date)].flatten + raise ClassError, errors unless errors.empty? + end +end diff --git a/models/reader.rb b/models/reader.rb new file mode 100644 index 0000000..18aeae4 --- /dev/null +++ b/models/reader.rb @@ -0,0 +1,43 @@ +class Reader + include Validator + + attr_reader :name, :email, :city, :street, :house + + def initialize(name: nil, email: nil, city: nil, street: nil, house: nil) + @name = name + @email = email + @city = city + @street = street + @house = house + end + + def validate! + check_presence + check_class + check_emptiness + check_positive + end + + private + + def check_presence + errors = validate_presence :name, :email, :city, :street, :house + raise PresenceError, errors unless errors.empty? + end + + def check_class + errors = [validate_class(String, :name, :email, :city, :street), + validate_class(Integer, :house)].flatten + raise ClassError, errors unless errors.empty? + end + + def check_emptiness + errors = validate_emptiness :name, :email, :city, :street, :house + raise EmptinessError, errors unless errors.empty? + end + + def check_positive + errors = validate_positive :house + raise PositiveNumberError, errors unless errors.empty? + end +end diff --git a/modules/statistics.rb b/modules/statistics.rb new file mode 100644 index 0000000..20ab8ea --- /dev/null +++ b/modules/statistics.rb @@ -0,0 +1,17 @@ +module Statistics + def top_readers(orders, quantity = 1) + top_objects(orders, :reader, quantity).keys.map(&:name) + end + + def top_books(orders, quantity = 1) + top_objects(orders, :book, quantity).keys.map(&:title) + end + + def number_readers_of_top_books(orders, quantity = 1) + top_objects(orders, :book, quantity).values.flatten.map(&:reader).uniq.size + end + + def top_objects(orders, object, quantity) + orders.group_by(&object).max_by(quantity) { |_key, value| value.size }.to_h + end +end diff --git a/modules/validation.rb b/modules/validation.rb new file mode 100644 index 0000000..af93e28 --- /dev/null +++ b/modules/validation.rb @@ -0,0 +1,29 @@ +module Validator + def validate_class(class_name, *attributes) + errors = attributes.map do |attr| + "Type of #{attr} must be an instance of #{class_name}" if public_send(attr).class != class_name + end + errors.compact + end + + def validate_presence(*attributes) + errors = attributes.map do |attr| + "#{attr} must be present" if public_send(attr).nil? + end + errors.compact + end + + def validate_emptiness(*attributes) + errors = attributes.map do |attr| + "#{attr} can't be blank" if public_send(attr) == '' + end + errors.compact + end + + def validate_positive(*attributes) + errors = attributes.map do |attr| + "#{attr} must be positive" if public_send(attr).to_i <= 0 + end + errors.compact + end +end