I am trying to build a database that has two principal tables build from csv input and link them together, but the associations are seemingly not being made – that is querying Language to get the family they belong returns nil every time.
I am either missing how Sequel works, or how should associations be construed in general.
My input data is:
language_families_csv
level
family_code
parent_code
meta_comment
1
INE
Indo-European
2
BAT
INE
Baltic
3
wes-BAT
BAT
Western Baltic
3
eas-BAT
BAT
Eastern Baltic
2
HYX
INE
Armenian
2
CEL
INE
Celtic
2
SLA
INE
Slavic
3
ZLE
SLA
East Slavic
3
ZLS
SLA
South Slavic
4
eas-ZLS
ZLS
Eastern South Slavic
4
wes-ZLS
ZLS
Western South Slavic
3
ZLW
SLA
West
4
leh-ZLW
ZLW
Lechitic
4
cze-ZLW
ZLW
Czech-Slovak
4
WEN
ZLW
Sorbian
languages_csv
language_code
parent_family
meta_comment
ENG
GMW
English
FRA
ROA
French
GER
GMW
German
SPA
ROA
Spanish
POL
leh-ZLW
Polish
CES
cze-ZLW
Czech
RUS
eas-ZLS
Russian
UKR
eas-ZLS
Ukrainian
BEL
eas-ZLS
Belarusian
CSB
leh-ZLW
Kashubian
POX
leh-ZLW
Polabian
SLK
cze-ZLW
Slovak
My schema:
module Language_to_LanguageFamily
def self.included(base)
base.one_to_many :languages, key: :parent_family, class: :Language
base.many_to_one :parent_family, class: :LanguageFamily, key: :family_code, primary_key: :family_code
end
end
### Language_Families ###
MyDB.create_table :Language_Families do
primary_key :language_family_id
Integer :level, index: true, null: false
String :family_code, size: 7, index: true, null: false, unique: true
String :meta_comment, text: true
# with a self-referential association to the primary key
foreign_key :parent_code, :Language_Families, key: :language_family_id
end
class LanguageFamily < Sequel::Model
plugin :timestamps, update_on_create: true, force: true
plugin :json_serializer
plugin :validation_helpers
# self-referrential association
one_to_many :children, class: :LanguageFamily, key: :parent_code
many_to_one :parent, class: :LanguageFamily, key: :parent_code
# external associations
include Language_to_LanguageFamily
end
### Languages ###
MyDB.create_table :Languages do
primary_key :language_id
String :language_code, size: 7, index: true, null: false, unique: true
String :meta_comment, text: true
# ↓ optional association
foreign_key :parent_family, :Language_Families, key: :family_code, null: true, default: nil
end
class Language < Sequel::Model
plugin :timestamps, update_on_create: true, force: true
plugin :json_serializer
plugin :validation_helpers
# external associations
include Language_to_LanguageFamily
# ↓ allows accessing foreign keys
attr_accessor :parent_family
end
and my code:
require 'csv'
require 'sequel'
MyDB = Sequel.connect adapter: :sqlite, database: 'MirDB.sqlite'
MyDB.drop_table(*db.tables)
# Create an empty hash to store the id of each language family code
code_id = {}
# insert data into Language_Families DB
language_families_csv.each do |family|
# Create a new LanguageFamily object
language_family = LanguageFamily.new
language_family.level = family["level"].to_i
language_family.family_code = family["family_code"]
language_family.meta_comment = family["meta_comment"]
if parent_code = family["parent_code"]
language_family.parent = LanguageFamily.find(parent_code: parent_code)
end
language_family.save
code_id[language_family.family_code] = language_family.id
end
# insert data into Language DB
languages_csv.each do |lang_row|
# Create a new Language object
language = Language.new
language.language_code = lang_row["language_code"]
language.meta_comment = lang_row["meta_comment"]
# set the association to the corresponding language family if present in languages
if lang_row["parent_family"] # ← if in languages
lang_parent_family = LanguageFamily.first(family_code: lang_row["parent_family"])
if lang_parent_family # ← if such family exists
language.parent_family = lang_parent_family
# puts "#{language.parent_family} ↔ #{lang_parent_family.family_code}"
else
puts 'Family '.red + lang_parent_family.family_code.cyan + ' doesn’t exist!'.red
end
end
language.save
end
Finally, when I run puts Language.all.to_s, all I get is:
[
#<Language #values={:language_id=>1, :language_code=>"ENG", :meta_comment=>"English", :parent_family=>nil}>,
#<Language #values={:language_id=>2, :language_code=>"FRA", :meta_comment=>"French", :parent_family=>nil}>,
#<Language #values={:language_id=>3, :language_code=>"GER", :meta_comment=>"German", :parent_family=>nil}>,
#<Language #values={:language_id=>4, :language_code=>"SPA", :meta_comment=>"Spanish", :parent_family=>nil}>,
#<Language #values={:language_id=>5, :language_code=>"POL", :meta_comment=>"Polish", :parent_family=>nil}>,
#<Language #values={:language_id=>6, :language_code=>"CES", :meta_comment=>"Czech", :parent_family=>nil}>,
#<Language #values={:language_id=>7, :language_code=>"RUS", :meta_comment=>"Russian", :parent_family=>nil}>,
#<Language #values={:language_id=>8, :language_code=>"UKR", :meta_comment=>"Ukrainian", :parent_family=>nil}>,
#<Language #values={:language_id=>9, :language_code=>"BEL", :meta_comment=>"Belarusian", :parent_family=>nil}>,
#<Language #values={:language_id=>10, :language_code=>"CSB", :meta_comment=>"Kashubian", :parent_family=>nil}>,
#<Language #values={:language_id=>11, :language_code=>"POX", :meta_comment=>"Polabian", :parent_family=>nil}>,
#<Language #values={:language_id=>12, :language_code=>"SLK", :meta_comment=>"Slovak", :parent_family=>nil}>,
(…)
]
I've uncomplicated your set up a bit. You have to make sure your code works, before you start extracting modules, it makes it that much harder to figure out what's going on. Also this is my first time using Sequel.
require "sequel"
require "csv"
db = Sequel.connect("sqlite://language.db")
db.drop_table(*db.tables)
# language_families languages
# .---------------. .---------------.
# .-->| code pk |<---. | code pk |
# | | name | | | name |
# | | level | `---| family_id fk |
# `---| parent_id fk | `---------------`
# `---------------`
db.create_table :language_families do
String :code, primary_key: true
Integer :level
String :name # `meta_comment` sure looks like `name` to me
String :parent_id # primary_key is a string so this is a string
end
db.create_table :languages do
String :code, primary_key: true
String :name
String :family_id
end
# you should avoid unnecessary prefixes, if you can:
# `family_code` in LanguageFamily, just `code` is sufficient
# `language_code` in Language, just `code` is sufficient
# `parent_family` in Language, just `family` is sufficient
# otherwise everything will be `parent_something` and `something_code`
# so far, i've learned this much: ActiveRecord equivalent:
# one_to_many has_many
# key: column from the other table foreign_key
# primary_key: column from the current table primary_key
#
# many_to_one belongs_to
# key: column from the current table foreign_key
# primary_key: column from the other table primary_key
# I don't see how `Language_to_LanguageFamily` can work for both models
class LanguageFamily < Sequel::Model
unrestrict_primary_key # because we need to assign primary key - `code`
many_to_one :parent, class: :LanguageFamily
one_to_many :children, class: :LanguageFamily, key: :parent_id
one_to_many :languages, key: :family_id
# making a full tree structure is more involved than parent/children
# relationships, this is just for a quick set up
def descendants
[self, children.map(&:descendants)].flatten
end
def descendant_languages
Language.where(family_id: descendants.map(&:code)).all
end
end
class Language < Sequel::Model
unrestrict_primary_key
many_to_one :family, class: :LanguageFamily
# from included module, i'm not sure how language has more languages, but if
# you mean other languages from the same family
def sibling_languages
return [] unless family
family.languages.reject { |lang| lang.code == self.code }
end
end
puts "==> Loading language families"
CSV.table("language_families.csv").each do |row|
language_family = LanguageFamily.new({
code: row[:family_code],
level: row[:level],
name: row[:meta_comment],
# this will set `parent_id`
parent: LanguageFamily.find(code: row[:parent_code])
})
p language_family.save
end
puts "==> Loading languages"
CSV.table("languages.csv").each do |row|
language = Language.new({
code: row[:language_code],
name: row[:meta_comment],
# this will set `family_id`
family: LanguageFamily.find(code: row[:parent_family])
})
# # if you want to do this as a separate step
# if (family = LanguageFamily.find(code: row[:parent_family]))
# language.family = family
# else
# puts "Family #{row[:parent_family]} doesn't exist!"
# end
p language.save
end
Test:
>> LanguageFamily.last.parent
=> #<LanguageFamily #values={:code=>"ZLS", :level=>3, :name=>"South Slavic", :parent_id=>"SLA"}>
>> LanguageFamily.find(code: "leh-ZLW").languages
=> [#<Language #values={:code=>"POL", :name=>"Polish", :family_id=>"leh-ZLW"}>,
#<Language #values={:code=>"CSB", :name=>"Kashubian", :family_id=>"leh-ZLW"}>,
#<Language #values={:code=>"POX", :name=>"Polabian", :family_id=>"leh-ZLW"}>]
>> LanguageFamily.find(code: "ZLW").languages
=> [] # no languages in this family, but plenty in child families
>> LanguageFamily.find(code: "ZLW").descendant_languages
=> [#<Language #values={:code=>"POL", :name=>"Polish", :family_id=>"leh-ZLW"}>,
#<Language #values={:code=>"CES", :name=>"Czech", :family_id=>"cze-ZLW"}>,
#<Language #values={:code=>"CSB", :name=>"Kashubian", :family_id=>"leh-ZLW"}>,
#<Language #values={:code=>"POX", :name=>"Polabian", :family_id=>"leh-ZLW"}>,
#<Language #values={:code=>"SLK", :name=>"Slovak", :family_id=>"cze-ZLW"}>]
# i guess you can also do "LIKE '%-ZLW'" ^
>> LanguageFamily.find(code: "ZLW").parent.parent.descendants.count
=> 15
>> Language.find(code: "POL").sibling_languages
=> [#<Language #values={:code=>"CSB", :name=>"Kashubian", :family_id=>"leh-ZLW"}>,
#<Language #values={:code=>"POX", :name=>"Polabian", :family_id=>"leh-ZLW"}>]
Import test:
>> load "app.rb"
==> Loading language families
#<LanguageFamily #values={:code=>"INE", :level=>1, :name=>"Indo-European", :parent_id=>nil}>
#<LanguageFamily #values={:code=>"BAT", :level=>2, :name=>"Baltic", :parent_id=>"INE"}>
#<LanguageFamily #values={:code=>"wes-BAT", :level=>3, :name=>"Western Baltic", :parent_id=>"BAT"}>
#<LanguageFamily #values={:code=>"eas-BAT", :level=>3, :name=>"Eastern Baltic", :parent_id=>"BAT"}>
#<LanguageFamily #values={:code=>"HYX", :level=>2, :name=>"Armenian", :parent_id=>"INE"}>
#<LanguageFamily #values={:code=>"CEL", :level=>2, :name=>"Celtic", :parent_id=>"INE"}>
#<LanguageFamily #values={:code=>"SLA", :level=>2, :name=>"Slavic", :parent_id=>"INE"}>
#<LanguageFamily #values={:code=>"ZLE", :level=>3, :name=>"East Slavic", :parent_id=>"SLA"}>
#<LanguageFamily #values={:code=>"ZLS", :level=>3, :name=>"South Slavic", :parent_id=>"SLA"}>
#<LanguageFamily #values={:code=>"eas-ZLS", :level=>4, :name=>"Eastern South Slavic", :parent_id=>"ZLS"}>
#<LanguageFamily #values={:code=>"wes-ZLS", :level=>4, :name=>"Western South Slavic", :parent_id=>"ZLS"}>
#<LanguageFamily #values={:code=>"ZLW", :level=>3, :name=>"West", :parent_id=>"SLA"}>
#<LanguageFamily #values={:code=>"leh-ZLW", :level=>4, :name=>"Lechitic", :parent_id=>"ZLW"}>
#<LanguageFamily #values={:code=>"cze-ZLW", :level=>4, :name=>"Czech-Slovak", :parent_id=>"ZLW"}>
#<LanguageFamily #values={:code=>"WEN", :level=>4, :name=>"Sorbian", :parent_id=>"ZLW"}>
==> Loading languages
#<Language #values={:code=>"ENG", :name=>"English", :family_id=>nil}>
#<Language #values={:code=>"FRA", :name=>"French", :family_id=>nil}>
#<Language #values={:code=>"GER", :name=>"German", :family_id=>nil}>
#<Language #values={:code=>"SPA", :name=>"Spanish", :family_id=>nil}>
#<Language #values={:code=>"POL", :name=>"Polish", :family_id=>"leh-ZLW"}>
#<Language #values={:code=>"CES", :name=>"Czech", :family_id=>"cze-ZLW"}>
#<Language #values={:code=>"RUS", :name=>"Russian", :family_id=>"eas-ZLS"}>
#<Language #values={:code=>"UKR", :name=>"Ukrainian", :family_id=>"eas-ZLS"}>
#<Language #values={:code=>"BEL", :name=>"Belarusian", :family_id=>"eas-ZLS"}>
#<Language #values={:code=>"CSB", :name=>"Kashubian", :family_id=>"leh-ZLW"}>
#<Language #values={:code=>"POX", :name=>"Polabian", :family_id=>"leh-ZLW"}>
#<Language #values={:code=>"SLK", :name=>"Slovak", :family_id=>"cze-ZLW"}>
=> true
Related
This is my ruby file with hash:
book.rb
class Book
attr_accessor :title, :author, :language, :classification, :isbn, :book_id, :borrow_status
def initialize(title, author, language, classification, isbn, book_id, borrow_status)
#title = title
#author = author
#language = language
#classification = classification
#isbn = isbn
#book_id = book_id
#borrow_status = borrow_status
end
def bookid
#book_id
end
def booklist
#title = #title.split(/ |\_|\-/).map(&:capitalize).join(" ")
#author = #author.split(/ |\_|\-/).map(&:capitalize).join(" ")
#language = #language.capitalize
#isbn.to_s
#book_id.to_s
{
:Title => #title,
:Author => #author,
:Language => #language,
:Classification => #classification,
:ISBN => #isbn,
:Book_ID => #book_id,
:Status => #borrow_status,
}
end
end
for now, I already have five key-value pair for this hash, they are in ruby file named top.rb:
$books1 = Book.new("lonely planet: ireland","damian harper","english","tourism",9781786574459,1,"available")
$books2 = Book.new("ninteen eighty four","george orwell","english","literature",9781374677817, 2,"available")
$books3 = Book.new("japanese in 30 days","naomi ono","japanese","education",9787928365729,3,"available")
$books4 = Book.new("brand famous: how to get everyone talking about your business","linzi boyd","english","business",9780857084903,4,"borrowed")
$books5 = Book.new("SQL in 10 mins","ming zhong, xiaoxia liu","chinese","hi tech",9787115313980,5,"unavailable")
and using the method below to output the result:
def status
bookstatus = gets.chomp.to_s
if bookstatus == "status"
puts "Status:" + "\n" + "#{$books1.booklist[:Book_ID]}: #{$books1.booklist[:Title]}: #{$books1.booklist[:Status]}"
puts "#{$books2.booklist[:Book_ID]}:#{$books2.booklist[:Title]}: #{$books2.booklist[:Status]}"
puts "#{$books3.booklist[:Book_ID]}:#{$books3.booklist[:Title]}: #{$books3.booklist[:Status]}"
puts "#{$books4.booklist[:Book_ID]}:#{$books4.booklist[:Title]}: #{$books4.booklist[:Status]}"
puts "#{$books5.booklist[:Book_ID]}:#{$books5.booklist[:Title]}: #{$books5.booklist[:Status]}"
else
puts "error"
end
end
For now, I am going to add some more value, I am going to let user enter the information of books (e.g., title = gets.chomp.to_s), and make a new key-value pair for the book added.
As I know, add new key-value pair to ruby is like below:
my_hash = {:a => 5}
my_hash[:key] = "value"
But, the hash in book.rb does not have any name, I tried to give it a name like
book_list = {
:Title => #title,
:Author => #author,
:Language => #language,
:Classification => #classification,
:ISBN => #isbn,
:Book_ID => #book_id,
:Status => #borrow_status,
}
it will output error.
My question is, I would like to know how can I add a new key-value pair to the hash in my ruby file, which has no name?
Thanks.
Changing the booklist method to this will solve your immediate problem:
def booklist
#booklist ||= {
:Title => #title.split(/ |\_|\-/).map(&:capitalize).join(" "),
:Author => #author.split(/ |\_|\-/).map(&:capitalize).join(" "),
:Language => #language.capitalize,
:Classification => #classification,
:ISBN => #isbn.to_s,
:Book_ID => #book_id.to_s,
:Status => #borrow_status,
}
end
Now you can do booklist[:Xyz] = 'xyz'.
The code in your question has so many "Hello world!" -level mistakes, that it appears you still seem to have a bit to learn about Ruby basics, such as using variables, the difference between #isbn, isbn, $isbn, ISBN, or ##isbn, etc.
There are better "learn ruby" tutorials online than I'm ready to start here, so my answer is probably not very helpful.
I am trying to compose an object Transaction from objects TranFee and Rate.
class Transaction
attr_reader :tranfee, :rate
def initialize(hash)
#tranfee = PaymentType::TranFee.new(hash)
#rate = PaymentType::Rate.new(hash)
end
end
module PaymentType
def initialize(args = {}, regex)
args.each do |key,value|
if key =~ regex
instance_variable_set("##{key}", value) unless value.nil?
eigenclass = class << self; self; end
eigenclass.class_eval do
attr_reader key
end
end
end
end
class TranFee
include PaymentType
def initialize(args, regex = /\Atran.*/)
super(args, regex)
end
end
class Rate
include PaymentType
def initialize(args, regex = /\Arate.*/)
super(args, regex)
end
end
end
The rate and TranFee objects are created from a hash like the one below.
reg_debit = {"name" => "reg_debit", "rate_base" => 0.0005,
"tran_fee" => 0.21, "rate_basis_points" => 0.002, "tran_auth_fee" => 0.10}
I am initializing the objects based on regex because the hash will eventually contain more values and I want the program to adjust as more items/classes are added.
Additionally there will be some instances where there are no key's starting with "tran". Does anyone know how to make Transaction create only a Rate object if TranFee has no instance variables inside of it? (in otherwords, if the hash returns nothing when keys =~ /\Atran.*/)
an example would be when the hash looks like this reg_debit = {"name" => "reg_debit", "rate_base" => 0.0005, "rate_basis_points" => 0.002}, right now the output is
#<Transaction:0x007ff98c070548 #tranfee=#<PaymentType::TranFee:0x007ff98c070520>, #rate=#<PaymentType::Rate:0x007ff98c0704a8 #rate_base=0.0005, #rate_basis_points=0.002>>
So I am getting a TranFee object with nothing in it and I would like for that to drop off in this situation. not sure if there may be a better way to design this? I was trying to think of a way to use ostruct or struct, but I havnt been able to figure it out. thanks for any help here.
I believe your strategy is very problematic - creating attributes to a class from user input doesn't sound like a very good idea.
Furthermore, adding methods (like attr_reader) to every instances can have severe performance issues.
If all you want is a data structure to hold your data, keep using a Hash. If you want a structure you can query using a dot notation instead of bracket notation, you might want to consider a gem like hashie or hashr.
If you want some code to make the flat data-structure hierarchical, I can suggest something like this:
hierarchical_hash = hash.each_with_object({}) do |(k, v), h|
if k.match(/^([^_]+)_(.+)$/)
root_key = $1
child_key = $2
h[root_key] ||= {}
h[root_key][child_key] = v
else
h[k] = v
end
end
# => {
# => "name" => "reg_debit",
# => "rate" => {
# => "base" => 0.0005,
# => "basis_points" => 0.002
# => },
# => "tran" => {
# => "fee" => 0.21,
# => "auth_fee" => 0.1
# => }
# => }
Your question raises some interesting issues. I will try to explain how you can fix it, but, as #Uri mentions, there may be better ways to address your problem.
I've assumed #tranfee is to be set equal to the first value in the hash whose key begins with "tran" and that #rate is to be set equal to the first value in the hash whose key begins with "rate". If that interpretation is not correct, please let me know.
Note that I've put initialize in the PaymentType module in a class (Papa) and made TranFee and Rate subclasses. That's the only way you can use super within initialize in the subclasses of that class.
Code
class Transaction
attr_reader :tranfee, :rate
def initialize(hash={})
o = PaymentType::TranFee.new(hash)
#tranfee = o.instance_variable_get(o.instance_variables.first)
o = PaymentType::Rate.new(hash)
#rate = o.instance_variable_get(o.instance_variables.first)
end
end
.
module PaymentType
class Papa
def initialize(hash, prefix)
key, value = hash.find { |key,value| key.start_with?(prefix) && value }
(raise ArgumentError, "No key beginning with #{prefix}") unless key
instance_variable_set("##{key}", value)
self.class.singleton_class.class_eval { attr_reader key }
end
end
class TranFee < Papa
def initialize(hash)
super hash, "tran"
end
end
class Rate < Papa
def initialize(hash)
super hash, "rate"
end
end
end
I believe the method Object#singleton_class has been available since Ruby 1.9.3.
Example
reg_debit = {"name" => "reg_debit", "rate_base" => 0.0005, "tran_fee" => 0.21,
"rate_basis_points" => 0.002, "tran_auth_fee" => 0.10}
a = Transaction.new reg_debit
p Transaction.instance_methods(false) #=> [:tranfee, :rate]
p a.instance_variables #=> [:#tranfee, :#rate]
p a.tranfee #=> 0.21
p a.rate #=> 0.0005
I have a Ruby script that generates a SQLite3 database.
I want to be able to generate an "output.csv" file containing one of the database tables.
Is there a way to handle that in Ruby?
It is easy with Sequel and to_csv:
require 'sequel'
DB = Sequel.sqlite
# since Sequel 3.48.0 to_csv is deprecated,
# we must load the to_csv feature via a extension
DB.extension(:sequel_3_dataset_methods) #define to_csv
DB.create_table(:test){
Fixnum :one
Fixnum :two
Fixnum :three
}
#Prepare some test data
5.times{|i|
DB[:test].insert(i,i*2,i*3)
}
File.open('test.csv', 'w'){|f|
f << DB[:test].to_csv
}
The result is:
one, two, three
0, 0, 0
1, 2, 3
2, 4, 6
3, 6, 9
4, 8, 12
In my test I had problems with line ends, so I needed an additional gsub:
File.open('test.csv', 'w'){|f|
f << DB[:test].to_csv.gsub("\r\n","\n")
}
If you want the export without the header line, use to_csv(false)
Remarks:
.to_csv is deprecated since Sequel 3.48.0 (2013-06-01).
You may use an old version with gem 'sequel', '< 3.48.0' or load the extension sequel_3_dataset_methods).
To get support for other seperators and other CSV-features you may use a combination of Sequel and CSV:
require 'sequel'
require 'csv'
#Build test data
DB = Sequel.sqlite
DB.create_table(:test){
Fixnum :one
Fixnum :two
Fixnum :three
String :four
}
#Prepare some test data
5.times{|i|
DB[:test].insert(i,i*2,i*3, 'test, no %i' % i)
}
#Build csv-file
File.open('test.csv', 'w'){|f|
DB[:test].each{|data|
f << data.values.to_csv(:col_sep=>';')
}
}
Result:
0;0;0;"test, no 0"
1;2;3;"test, no 1"
2;4;6;"test, no 2"
3;6;9;"test, no 3"
4;8;12;"test, no 4"
As an alternative you may patch Sequel::Dataset (modified code from a post of marcalc at Github):
class Sequel::Dataset
require 'csv'
#
#Options:
#* include_column_titles: true/false. default true
#* Other options are forwarded to CSV.generate
def to_csv(options={})
include_column_titles = options.delete(:include_column_titles){true} #default: true
n = naked
cols = n.columns
csv_string = CSV.generate(options) do |csv|
csv << cols if include_column_titles
n.each{|r| csv << cols.collect{|c| r[c] } }
end
csv_string
end
end
# Assume that model is an activerecord model
#secrets = Model.all
#csv = CSV.generate do |csv|
#secrets.each { |secret|
csv << ["#{secret.attr1.to_s}", "#{secret.attr2.to_s"] # and so on till your row is finished
}
end
render :text => #csv, :content_type => 'application/csv'
If you have further problems, leave a comment.
Adding an update for 2020. Since Sequel v5, sequel_3_dataset_methods has been completely removed and is unavailable. As such, generating a CSV as a Database extension has also been completely removed.
It appears the current "best practice" is to add the csv_serializer plugin to a Sequel::Model class. There is a catch here though, that the Sequel::Model class you define must be defined after the call to Sequel.connect. The act of subclassing Sequel::Model invokes a read from the database.
This prevents a typical workflow of pre-defining your classes as part of any generic Gem.
According to the Sequel author, the preferred way to do this is through MyClass = Class.new(Sequel::Model(:tablename)) in-line, or otherwise only calling require within your method definitions.
Making no promises about efficiency, here is a code sample that defines 'best practice'
require 'sequel'
require 'csv'
module SequelTsv
class One
def self.main
db = Sequel.connect('sqlite://blog.db') # requires sqlite3
db.create_table :items do
primary_key :id
String :name
Float :price
end
items = db[:items] # Create a dataset
items.insert(:name => 'abc', :price => rand * 100)
items.insert(:name => 'def', :price => rand * 100)
items.insert(:name => 'ghi', :price => rand * 100)
item_class = Class.new(Sequel::Model(:items))
item_class.class_eval do
plugin :csv_serializer
end
tsv = item_class.to_csv(write_headers: true, col_sep:"\t")
CSV.open('output.tsv', 'w') do |csv|
CSV.parse(tsv) do | c |
csv << c
end
end
end
end
end
SequelTsv::One.main
output:
id name price
1 abc 39.307899453608364
2 def 99.28471503410731
3 ghi 58.0295131255661
Let's say I want to group States into Regions. For instance, the Southwest Region might be: Texas, Oklahoma, Colorado, New Mexico, Utah, Arizona, Nevada.
Somehow I need to create the list of states and define the region groupings. I also need to be able to lookup a region given the name of a state, something like region_for('Texas') which would return 'Southwest'.
What is the best, cleanest, "Ruby Way" to do something like this? I'd like to do it using plain 'ol ruby, no database or frameworks.
You can almost type this data structure directly into Ruby...
result = {
'Southwest' => %W{Texas Oklahoma Colorado New\ Mexico Utah Arizona Nevada},
'West' => %W{California Oregon Washington},
}.inject({}) do |m, (k, v)|
m[k] = v
v.each { |s| m[s] = k }
m
end
This produces a single Hash that has both states and regions as keys identifying each other. The data structure looks something like:
{"Colorado" => "Southwest",
"New Mexico" => "Southwest",
"Oklahoma" => "Southwest",
"California" => "West",
"Oregon" => "West",
"Texas" => "Southwest",
"Washington" => "West",
"Utah" => "Southwest",
"Nevada" => "Southwest",
"Arizona" => "Southwest"
"Southwest" =>
["Texas", "Oklahoma", "Colorado", "New Mexico", "Utah", "Arizona", "Nevada"],
"West" =>
["California", "Oregon", "Washington"],
}
Another approach would create a separate hash for states. Then you could get a list of regions or states by using Hash#keys, but you could do that here as well by using Enumerable#select or Enumerable#reject based on the type of the value.
The 'pure' ruby way is just to use hashes, and then have keys to do your lookups. There's a gem that kind of does something like this: ruport. It might be worth it to look at the source-code. For the use case you've illustrated, I'd have something like:
class RegionMapper
#potentially put this in a config file
REGIONS = Hash[[['California', 'Southwest'], ...]]
def initialize
#region_map = REGIONS.inject({}) {|r, e| r[e.second] ||= []; r[e.second] << e.first; r}
end
def region_for_state(state)
REGIONS[state]
end
def states_for_region(region)
#region_map(region)
end
end
The point is, to be efficient, you want to have a hash to do the lookups on each key you want to search by. But you dont' want to expose the data-duplication, so you put it all in a class.
If you have multiple values / keys, then you really have a table. If you want to keep constant time lookups, then you build a hash for each column (like the #region_map)
Try:
class State
attr_accessor :name, :region
def initialize(name, region=nil)
#name = name
#region = region
end
end
class Region
attr_accessor :name, :states
def initialize(name, states)
#name = name
#states = states
end
def set_state_regions
self.states.each {|state| state.region = self.name}
end
end
mo = State.new("missouri")
il = State.new("illionois")
oh = State.new("ohio")
midwest = Region.new("midwest", [mo, il, oh])
midwest.states.each {|state| puts state.name}
midwest.set_state_regions
I may come back and reflect on this later, I think it violates some OO principles.
I builded a very similar answer like Caley.
Main difference: I stored my data in a yaml-structure.
require 'yaml'
class Region
##all = {}
def self.[](key)
##all[key]
end
def initialize(name)
#name = name
#states = []
##all[#name] = self
end
def <<(state)
#states << state
state.region = state
end
def each_state
#states.each{|state| yield state } if block_given?
#states
end
attr_reader :name
end
class State
##all = {}
def self.[](key)
##all[key]
end
def initialize(name, region = nil)
#name = name
#region = region
##all[#name] = self
end
attr_accessor :name
attr_accessor :region
end
YAML.load(DATA).each{|region,states|
r = Region.new(region)
states.each{|state| r << State.new(state) }
}
p Region['Southwest Region']
p Region['Southwest Region'].each_state
Region['Southwest Region'].each_state{|state|
p state.name
}
__END__
Southwest Region:
- Texas
- Oklahoma
- Colorado
- New Mexico
- Utah
- Arizona
- Nevada.
Pacific:
- California
- Oregon
- Washington
A Hash is fine, you don't need anything fancier for this.
region = {
"Maine" => "New England",
"New Hampshire" => "New England",
etc
}
Which you then use like
region["Maine"]
Or if you want to set it up more compactly, like this:
regions = {
"Southwest" => ["Texas", "Oklahoma", "Colorado", "New Mexico", "Utah", "Arizona", "Nevada"],
"New England" => ["Maine", "New Hampshire", "Vermont", "Massachusetts","Rhode Island", "Connecticut"],
etc
}
region = {}
regions.each do |r,states|
states.each do |state|
region[state] = r
end
end
I have four arrays that are coming in from the client. Let's say that there is an array of names, birth dates, favorite color and location. The idea is I want a hash later where each name will have a hash with respective attributes:
Example date coming from the client:
[name0, name1, name2, name3]
[loc0, loc1]
[favcololor0, favcolor1]
[bd0, bd1, bd2, bd3, bd4, bd5]
Output I'd like to achieve:
name0 => { location => loc0, favcolor => favcolor0, bd => bd0 }
name1 => { location => loc1, favcolor => favcolor1, bd => bd1 }
name2 => { location => nil, favcolor => nil, bd => bd2 }
name3 => { location => nil, favcolor => nil, bd => bd3 }
I want to have an array at the end of the day where I can iterate and work on each particular person hash.
There need not be an equivalent number of values in each array. Meaning, names are required.. and I might receive 5 of them, but I only might receive 3 birth dates, 2 favorite colors and 1 location. Every missing value will result in a nil.
How does one make that kind of data structure with Ruby 1.9?
I would probably do it like this
# assuming names, fav_colors, birth_dates, and locations are your arrays
name_collection = {}
names.zip(birth_dates, fav_colors, locations) do |name, birth_date, fav_color, location|
name_collection[name] = { :birth_date => birth_date,
:fav_color => fav_color,
:location => location }
end
# usage
puts name_collection['jack'][:fav_color] # => 'blue'
A small class to represent a person
class Person
attr_accessor :name, :color, :loc, :bd
def initialize(args = {})
#name = args[:name]
#color = args[:color]
#loc = args[:loc]
#bd = args[:bd]
end
def pp()
puts "*********"
puts "Name: #{#name}"
puts "Location: #{#loc}"
puts "Birthday: #{#bd}"
puts "Fav. Color: #{#color}"
puts "*********"
end
end
another to represent people, which is mainly just a listing of Persons.
class People
attr_accessor :list_of_people
def initialize()
#list_of_people = {}
end
def load_people(names, locations, favcolors, birthdates)
names.each_with_index do |name, index|
#list_of_people[name] = Person.new(:name => name, :color => favcolors[index], :loc => locations[index], :bd => birthdates[index])
end
end
def pp()
#list_of_people.each_pair do |key, value|
value.pp()
end
end
end
I threw in a pretty print function for each so you can see their data. With a starting point like this it will be really easy to modify and add methods that do all sorts of useful things.
if __FILE__ == $0
names = ["name0", "name1", "name2", "name3"]
locs = ["loc0","loc1"]
favcolors = ["favcolor0", "favcolor1"]
bds = ["bd0","bd1","bd2","bd3","bd4"]
a = People.new()
a.load_people(names,locs,favcolors,bds)
a.pp()
end
I think the kind of data structure you're looking for is -ahem- a Struct.
# setup data
names = %w(name0 name1 name2 name3)
locations = %w(loc0 loc1)
colors = %w(favcololor0 favcolor1)
bd = %w(bd0 bd1 bd2 bd3 bd4 bd5)
# let's go
Person = Struct.new( :name, :location, :fav_color, :bd )
all_persons = names.zip( locations, colors, bd ).map{|p| Person.new( *p)}
# done
puts all_persons
someone= all_persons.find{|p| p.name == "name1"}
puts someone.location unless someone.nil?