Ruby setter idiom - ruby

I'm working on a Chart class and it has a margin parameter, that holds :top, :bottom, :right and :left values. My first option was to make margin a setter and set values like this:
# Sets :left and :right margins and doesn't alter :top and :bottom
chart.margins = {:left => 10, :right => 15}
It's nice, because it is clearly a setter, but, after some thought, I think it could be confusing too: the user might think that margins contains only :left and :right values, what is not right. Another option is eliminate = and make it an ordinary method:
chart.margins(:left => 10, :right => 15)
With this syntax, it's easy to figure out what is happening, but it is not a standard setter and conflicts with margins getter. And there's still another option:
chart.margins(:left, 10)
chart.margins(:right, 15)
I don't know what to think about this. For me, it is obvious the method is a setter, but this time I cannot set multiple values with just one call and there's the problem with getter again. I'm relatively new to Ruby and I haven't got used to all the idioms yet. So, what do you think guys? Which is the best option?

You could also make a Margin class to enjoy the following clear syntax:
class Margin
attr_accessor :left, :right, :top, :bottom
...
end
class Chart
attr_accessor :margins
...
end
chart.margins.left = 10
puts chart.margins.right

Not so sure if this is the kind of syntax that you would like to make available (sorry if not : )
#!/usr/bin/ruby
class Margins < Struct.new(:top, :bottom, :left, :right)
end
class Chart
attr_reader :margins
def initialize()
#margins = Margins.new(0,0,0,0)
end
def margins=(hash)
[:top, :bottom, :left, :right].each do |dir|
if (hash[dir])
#margins[dir] = hash[dir]
end
end
end
end
c = Chart.new
c.margins.left = 10
c.margins={:top=>12,:bottom=>13}
puts c.margins.left
# 10
puts c.inspect;
# #<Chart:0xb7caaf8c #margins=#<struct Margins top=12, bottom=13, left=10, right=0>>
# However c.margins.foo = 12 would give you an error

In addition to paradigmatic's answer, you could add a method to the Margins class to support:
chart.margins.set :left => 10, :right => 15
You could extend the margins= method to treat a numeric argument:
chart.margins = 20
as sugar for:
chart.margins = Margins.new(20, 20, 20, 20)

I don't think creating a class for Margin is an overkill. You can always expose its values as a hash using to_hash or something similar.
Also, if you like, you can make it work in DSL-style:
chart.margins do |m|
m.left 10
m.right 20
m.vertical 5 # sets both top and bottom margin
end
But I guess I would choose paradigmatic's approach anyway...

You could also stick with what you had first and use the normal hash syntax.
margins["left"] = 10 #to set just one without changing the others

Related

`no implicit conversion of nil into String` wih ruby2d

So for practice with ruby, I am creating a library on top of ruby2d, but one problem.
I keep getting an error, I think setting up a window, here is the full error:
C:/Ruby31-x64/lib/ruby/gems/3.1.0/gems/ruby2d-0.11.3/lib/ruby2d/window.rb:476:in `exist?': no implicit conversion of nil into String (TypeError)
from C:/Ruby31-x64/lib/ruby/gems/3.1.0/gems/ruby2d-0.11.3/lib/ruby2d/window.rb:476:in `add_controller_mappings'
from C:/Ruby31-x64/lib/ruby/gems/3.1.0/gems/ruby2d-0.11.3/lib/ruby2d/window.rb:629:in `ext_show'
from C:/Ruby31-x64/lib/ruby/gems/3.1.0/gems/ruby2d-0.11.3/lib/ruby2d/window.rb:629:in `show'
from C:/Users/Admin/Desktop/RubyProjects/RSGE/rsge_window.rb:19:in `mainloop'
from c:/Users/Admin/Desktop/RubyProjects/test.rb:4:in `<main>'
it shows that something is wrong in ruby2d (a graphics library for ruby)
here is the code for my test file that uses my library
require './RSGE/rsge'
root = Window.new(800, 800)
root.mainloop
and here is my window class:
require 'ruby2d'
require_relative 'rsge_colors'
class Window
def initialize (w, h)
#width = w.to_i
#height = h.to_i
#title = 'Blank Window'
#background = $WHITE
end
def config
set width: #width, height: #height
set background: #background
set title: #title
end
def mainloop
show
end
end
if your wondering what rsge_colors is, here it is
$RED = 'red'
$GREEN = 'green'
$BLUE = 'blue'
$ORANGE = 'orange'
$PURPLE = 'purple'
$YELLOW = 'yellow'
$WHITE = 'white'
$BLACK = 'black'
Why am I getting this error, and how can I fix it?
Thank you for any help you can give me!
Defining class Window looks like you're overriding Ruby2D::Window class.
I've never used this library, but looking the code for some of the games in the showcase, and also the "Get started" section, it looks like the dsl must be used in the root level of your ruby file.
In your case, it should be something like:
require 'ruby2d'
require_relative 'rsge_colors'
set width: 800, height: 800
set background: $WHITE
set title: 'Blank Window'
show
Probably there must be a way to encapsulate the code into classes or modules, but if you're just learning to use the gem, probably would be better to start with the basic stuff then move to refactor these type of things.

Centering text in Gosu

I've been having trouble centering text in the Gosu library to the absolute middle of the screen.
require 'gosu'
class GameWindow < Gosu::Window
def initialize (width=800, height=600, fullscreen=false)
super
self.caption = 'Hello'
#message = Gosu::Image.from_text(
self, 'HELLO WORLD', Gosu.default_font_name, 45)
end
def draw
#message.draw(377.5,277.5,0)
end
end
window = GameWindow.new
window.show
My first approach was to take the height of the screen, subtract it by the height of the text 45, and then divide by 2. Now that seemed to work when aligning vertically.
However, horizontally is a different story...It seems to
be taking the top left corner of the text and centering it which I expected it to do, instead of the middle of the text.
Anyone got a formula for this ? I tried a whole bunch of things, and only came close.
class GameWindow < Gosu::Window
def initialize (width=800, height=600, fullscreen=false)
super
self.caption = 'Hello'
#message = Gosu::Image.from_text(
self, 'HELLO WORLD', Gosu.default_font_name, 45)
end
def draw
#message.draw(377.5,277.5,0)
end
end
Your #message is an instance of Gosu::Image
As far as I can see, the class has a method that allows you to align the image's rotational center to a specified point, draw_rot
Using draw_rot instead of draw should work for you once you've found the center of the frame.
I know this is an old question, but I was having this issue earlier today and came up with this solution.
def draw_centered_text(text, size, font)
centered_text = Gosu::Image.from_text(text, size, {:width => WIDTH, :align => :center, :font => font})
end
The above function converts the passed text to an image with a width equal to WIDTH (which in my case is a constant that stores the window width) and the text centred. You can then call the function like so:
draw_centered_text("Your text", 20, "Arial Bold").draw(0, 50, 0, 1, 1, Gosu::Color::WHITE)
You can replace 20 and 50 with any line height (font size) and y-position you want, just as you can change "Arial Bold" to "Arial" or any other font on your system. However, keep the 0 for the x-position (first parameter of draw()), since the centred text image is the same width as the window width.
See the links below for further information on from_text() and draw():
https://www.rubydoc.info/github/gosu/gosu/Gosu/Image#from_text-class_method
https://www.rubydoc.info/github/gosu/gosu/Gosu/Image#draw-instance_method
Better late than never...
No need to convert your text to an image. Just center the text using two parameters available on the Font.draw_text_rel method: rel_x and rel_y. See your code (modified a bit) below.
See: https://www.rubydoc.info/gems/gosu/Gosu%2FFont:draw_text_rel
require 'gosu'
class GameWindow < Gosu::Window
def initialize (width=800, height=600, fullscreen=false)
super
self.caption = 'Hello'
# #message = Gosu::Image.from_text(
# self, 'HELLO WORLD', Gosu.default_font_name, 45)
#font = Gosu::Font.new(45)
#message = "HELLO WORLD"
end
def draw
#font.draw_text_rel(#message, width / 2, height / 2, 1, rel_x = 0.5, rel_y = 0.5)
end
end
window = GameWindow.new
window.show

Controlling content flow with Prawn

Let's say we want to display a title on the first page that takes up the top half of the page. The bottom half of the page should then fill up with our article text, and the text should continue to flow over into the subsequent pages until it runs out:
This is a pretty basic layout scenario but I don't understand how one would implement it in Prawn.
Here's some example code derived from their online documentation:
pdf = Prawn::Document.new do
text "The Prince", :align => :center, :size => 48
text "Niccolò Machiavelli", :align => :center, :size => 20
move_down 42
column_box([0, cursor], :columns => 3, :width => bounds.width) do
text((<<-END.gsub(/\s+/, ' ') + "\n\n") * 20)
All the States and Governments by which men are or ever have been ruled,
have been and are either Republics or Princedoms. Princedoms are either
hereditary, in which the bla bla bla bla .....
END
end
end.render
but that will just continue to show the title space for every page:
What's the right way to do this?
I have been fighting with this same problem. I ended up subclassing ColumnBox and adding a helper to invoke it like so:
module Prawn
class Document
def reflow_column_box(*args, &block)
init_column_box(block) do |parent_box|
map_to_absolute!(args[0])
#bounding_box = ReflowColumnBox.new(self, parent_box, *args)
end
end
private
class ReflowColumnBox < ColumnBox
def move_past_bottom
#current_column = (#current_column + 1) % #columns
#document.y = #y
if 0 == #current_column
#y = #parent.absolute_top
#document.start_new_page
end
end
end
end
end
Then it is invoked exactly like a normal column box, but on the next page break will reflow to the parents bounding box. Change your line:
column_box([0, cursor], :columns => 3, :width => bounds.width) do
to
reflow_column_box([0, cursor], :columns => 3, :width => bounds.width) do
Hope it helps you. Prawn is pretty low level, which is a two-edged sword, it sometimes fails to do what you need, but the tools are there to extend and build more complicated structures.
I know this is old, but I thought I'd share that a new option has been added to fix this in v0.14.0.
:reflow_margins is an option that sets column boxes to fill their parent boxes on new page creation.
column_box(reflow_margins: true, columns: 3)
So, the column_box method creates a bounding box. The documented behavior of the bounding box is that it starts at the same position as on the previous page if it changes to the next page. So the behavior you are seeing is basically correct, also not what you want. The suggested workaround I have found by googling is to use a span instead, because spans do not have this behavior.
The problem now is, how to build text columns with spans? They don't seem to support spans natively. I tried to build a small script that mimicks columns with spans. It creates one span for each column and aligns them accordingly. Then, the text is written with text_box, which has the overflow: :truncate option. This makes the method return the text that did not fit in the text box, so that this text can then be rendered in the next column. The code probably needs some tweaking, but it should be enough to demonstrate how to do this.
require 'prawn'
text_to_write = ((<<-END.gsub(/\s+/, ' ') + "\n\n") * 20)
All the States and Governments by which men are or ever have been ruled,
have been and are either Republics or Princedoms. Princedoms are either
hereditary, in which the bla bla bla bla .....
END
pdf = Prawn::Document.generate("test.pdf") do
text "The Prince", :align => :center, :size => 48
text "Niccolò Machiavelli", :align => :center, :size => 20
move_down 42
starting_y = cursor
starting_page = page_number
span(bounds.width / 3, position: :left) do
text_to_write = text_box text_to_write, at: [bounds.left, 0], overflow: :truncate
end
go_to_page(starting_page)
move_cursor_to(starting_y)
span(bounds.width / 3, position: :center) do
text_to_write = text_box text_to_write, at: [bounds.left, 0], overflow: :truncate
end
go_to_page(starting_page)
move_cursor_to(starting_y)
span(bounds.width / 3, position: :right) do
text_box text_to_write, at: [bounds.left, 0]
end
end
I know this is not an ideal solution. However, this was the best I could come up with.
Use floats.
float do
span((bounds.width / 3) - 20, :position => :left) do
# Row Table Code
end
end
float do
span((bounds.width / 3) - 20, :position => :center) do
# Row Table Code
end
end
float do
span((bounds.width / 3) - 20, :position => :right) do
# Row Table Code
end
end
Use Prawns grid layout instead. It is very well documented...and easier to control your layout.

List dynamic attributes in a Mongoid Model

I have gone over the documentation, and I can't find a specific way to go about this. I have already added some dynamic attributes to a model, and I would like to be able to iterate over all of them.
So, for a concrete example:
class Order
include Mongoid::Document
field :status, type: String, default: "pending"
end
And then I do the following:
Order.new(status: "processed", internal_id: "1111")
And later I want to come back and be able to get a list/array of all the dynamic attributes (in this case, "internal_id" is it).
I'm still digging, but I'd love to hear if anyone else has solved this already.
Just include something like this in your model:
module DynamicAttributeSupport
def self.included(base)
base.send :include, InstanceMethods
end
module InstanceMethods
def dynamic_attributes
attributes.keys - _protected_attributes[:default].to_a - fields.keys
end
def static_attributes
fields.keys - dynamic_attributes
end
end
end
and here is a spec to go with it:
require 'spec_helper'
describe "dynamic attributes" do
class DynamicAttributeModel
include Mongoid::Document
include DynamicAttributeSupport
field :defined_field, type: String
end
it "provides dynamic_attribute helper" do
d = DynamicAttributeModel.new(age: 45, defined_field: 'George')
d.dynamic_attributes.should == ['age']
end
it "has static attributes" do
d = DynamicAttributeModel.new(foo: 'bar')
d.static_attributes.should include('defined_field')
d.static_attributes.should_not include('foo')
end
it "allows creation with dynamic attributes" do
d = DynamicAttributeModel.create(age: 99, blood_type: 'A')
d = DynamicAttributeModel.find(d.id)
d.age.should == 99
d.blood_type.should == 'A'
d.dynamic_attributes.should == ['age', 'blood_type']
end
end
this will give you only the dynamic field names for a given record x:
dynamic_attribute_names = x.attributes.keys - x.fields.keys
if you use additional Mongoid features, you need to subtract the fields associated with those features:
e.g. for Mongoid::Versioning :
dynamic_attribute_names = (x.attributes.keys - x.fields.keys) - ['versions']
To get the key/value pairs for only the dynamic attributes:
make sure to clone the result of attributes(), otherwise you modify x !!
attr_hash = x.attributes.clone #### make sure to clone this, otherwise you modify x !!
dyn_attr_hash = attr_hash.delete_if{|k,v| ! dynamic_attribute_names.include?(k)}
or in one line:
x.attributes.clone.delete_if{|k,v| ! dynamic_attribute_names.include?(k)}
So, what I ended up doing is this. I'm not sure if it's the best way to go about it, but it seems to give me the results I'm looking for.
class Order
def dynamic_attributes
self.attributes.delete_if { |attribute|
self.fields.keys.member? attribute
}
end
end
Attributes appears to be a list of the actual attributes on the object, while fields appears to be a hash of the fields that were predefined. Couldn't exactly find that in the documentation, but I'm going with it for now unless someone else knows of a better way!
try .methods or .instance_variables
Not sure if I liked the clone approach, so I wrote one too. From this you could easily build a hash of the content too. This merely outputs it all the dynamic fields (flat structure)
(d.attributes.keys - d.fields.keys).each {|a| puts "#{a} = #{d[a]}"};
I wasn't able to get any of the above solutions to work (as I didn't want to have to add slabs and slabs of code to each model, and, for some reason, the attributes method does not exist on a model instance, for me. :/), so I decided to write my own helper to do this for me. Please note that this method includes both dynamic and predefined fields.
helpers/mongoid_attribute_helper.rb:
module MongoidAttributeHelper
def self.included(base)
base.extend(AttributeMethods)
end
module AttributeMethods
def get_all_attributes
map = %Q{
function() {
for(var key in this)
{
emit(key, null);
}
}
}
reduce = %Q{
function(key, value) {
return null;
}
}
hashedResults = self.map_reduce(map, reduce).out(inline: true) # Returns an array of Hashes (i.e. {"_id"=>"EmailAddress", "value"=>nil} )
# Build an array of just the "_id"s.
results = Array.new
hashedResults.each do |value|
results << value["_id"]
end
return results
end
end
end
models/user.rb:
class User
include Mongoid::Document
include MongoidAttributeHelper
...
end
Once I've added the aforementioned include (include MongoidAttributeHelper) to each model which I would like to use this method with, I can get a list of all fields using User.get_all_attributes.
Granted, this may not be the most efficient or elegant of methods, but it definitely works. :)

How to set DataMapper String length to unlimited for PostgreSQL

Granted, one could use property :foo, Text, lazy: false all over the place to replace property :foo, String but that, of course, defeats several purposes in one go. Or I could use manual migrations, which I have been doing—I'm looking around now to see if they can finally be abandoned, insofar as VARCHAR v. TEXT is concerned.
In other words, I'd like automigrate to create TEXT fields for PostgreSQL for models with String properties rather than arbitrarily, pointlessly, constrained VARCHAR atop a TEXT.
The Postgres adapter seems to be able to handle it:
# size is still required, as length in postgres behaves slightly differently
def size
case self.type
#strings in postgres can be unlimited length
when :string then return (#options.has_key?(:length) || #options.has_key?(:size) ? #size : nil)
else nil
end
end
Untested suggestion, but considering the source of initialize, and the length function immediately beneath it, try passing nil as the length, or no length at all.
Here's a lousy but working solution I came up with—if you've better one please add it and I'll accept that instead.
In config/initializers/postgresql_strings.rb
module DataMapper
module Migrations
module PostgresAdapter
def self.included(base)
base.extend ClassMethods
end
module ClassMethods
def type_map
precision = Property::Numeric.precision
scale = Property::Decimal.scale
super.merge(
Property::Binary => { :primitive => 'BYTEA' },
BigDecimal => { :primitive => 'NUMERIC', :precision => precision, :scale => scale },
Float => { :primitive => 'DOUBLE PRECISION' },
String => { :primitive => 'TEXT' } # All that for this
).freeze
end
end
end
end
end
# If you're including dm-validations, it will surprisingly attempt
# to validate strings to <= 50 characters, this prevents that.
DataMapper::Property::String.auto_validation(false)

Resources