Related
I would like to create a custom scanner for i18n-tasks that can detect enums declared as hashes in models.
My enum declaration pattern will always be like this:
class Conversation < ActiveRecord::Base
enum status: { active: 0, archived: 1}, _prefix: true
enum subject: { science: 0, literature: 1, music: 2, art: 3 }, _prefix: true
end
The enums will always be declared as hashes, and will always have a numerical hash value, and will always have the option _prefix: true at the end of the declaration. There can be any number of values in the hash.
My custom scanner currently looks like this:
require 'i18n/tasks/scanners/file_scanner'
class ScanModelEnums < I18n::Tasks::Scanners::FileScanner
include I18n::Tasks::Scanners::OccurrenceFromPosition
# #return [Array<[absolute key, Results::Occurrence]>]
def scan_file(path)
text = read_file(path)
text.scan(/enum\s([a-zA-Z]*?):\s\{.*\W(\w+):.*\}, _prefix: true$/).map do |prefix, attribute|
occurrence = occurrence_from_position(
path, text, Regexp.last_match.offset(0).first)
model = File.basename(path, ".rb") #.split('/').last
name = prefix + "_" + attribute
["activerecord.attributes.%s.%s" % [model, name], occurrence]
end
end
end
I18n::Tasks.add_scanner 'ScanModelEnums'
However this is only returning the very last element of each hash:
activerecord.attributes.conversation.status_archived
activerecord.attributes.conversation.subject_art
How can I return all the elements of each hash? I am wanting to see a result like this:
activerecord.attributes.conversation.status_active
activerecord.attributes.conversation.status_archived
activerecord.attributes.conversation.subject_science
activerecord.attributes.conversation.subject_literature
activerecord.attributes.conversation.subject_music
activerecord.attributes.conversation.subject_art
For reference, the i18n-tasks github repo offers an example of a custom scanner.
The file scanner class that it uses can be found here.
This works:
def scan_file(path)
result = []
text = read_file(path)
text.scan(/enum\s([a-zA-Z]*?):\s\{(.*)}, _prefix: true$/).each do |prefix, body|
occurrence = occurrence_from_position(path, text,
Regexp.last_match.offset(0).first)
body.scan(/(\w+):/).flatten.each do |attr|
model = File.basename(path, ".rb")
name = "#{prefix}_#{attr}"
result << ["activerecord.attributes.#{model}.#{name}", occurrence]
end
end
result
end
It's similar to your 'answer' approach, but uses the regex to get all the contents between '{...}', and then uses another regex to grab each enum key name.
The probable reason your 'answer' version raises an error is that it is actually returning a three-dimensional array, not two:
The outer .map is an array of all iterations.
Each iteration returns retval, which is an array.
Each element of retail is an array of ['key', occurrence] pairs.
This isn't the answer, this is just the other attempt I made, which outputs a two dimensional array instead of a single array:
require 'i18n/tasks/scanners/file_scanner'
class ScanModelEnums < I18n::Tasks::Scanners::FileScanner
include I18n::Tasks::Scanners::OccurrenceFromPosition
# #return [Array<[absolute key, Results::Occurrence]>]
def scan_file(path)
text = read_file(path)
text.scan(/enum\s([a-zA-Z]*?):\s\{(.*)\}, _prefix: true/).map do |prefix, attributes|
retval = []
model = File.basename(path, ".rb")
names = attributes.split(",").map!{ |e| e.strip; e.split(":").first.strip }
names.each do |attribute|
pos = (Regexp.last_match.offset(0).first + 8 + prefix.length + attributes.index(attribute))
occurrence = occurrence_from_position(
path, text, pos)
name = prefix + "_" + attribute
# p "================"
# p type
# p message
# p ["activerecord.attributes.%s.%s" % [model, name], occurrence]
# p "================"
retval.push(["activerecord.attributes.%s.%s" % [model, name], occurrence])
end
retval
end
end
end
I18n::Tasks.add_scanner 'ScanModelEnums'
This however gives me an error for the second detected attribute:
gems/i18n-tasks-0.9.34/lib/i18n/tasks/scanners/results/key_occurrences.rb:48:in `each': undefined method `path' for ["activerecord.attributes.conversation.status_archived", Occurrence(app/models/project.rb:3:32:98::)]:Array (NoMethodError)
I want to convert all the values in a nested hash to a utf8 compatible string. I initially thought this would be easy and something like deep_apply should be available for me to use, but I am unable to find anything this simple on a quick google and SO search.
I do not want to write (maintain) a method similar to the lines of Change values in a nested hash . Is there a native API implementation or a shorthand available for this or do I have to write my own method?
I ended up implementing my own approach, that is in no way perfect but works well for my use case and should be easy to maintain. Posting it here for reference to anyone who wants to try it out
def deep_apply object, klasses, &blk
if object.is_a? Array
object.map { |obj_ele| deep_apply(obj_ele, klasses, &blk) }
elsif object.is_a? Hash
object.update(object) {|_, value| deep_apply(value, klasses, &blk) }
elsif klasses.any? { |klass| object.is_a? klass }
blk.call(object)
else
object
end
end
usage:
=> pry(main)> deep_apply({a: [1, 2, "sadsad"]}, [String, Integer]) { |v| v.to_s + "asd" }
=> {:a=>["1asd", "2asd", "sadsadasd"]}
Interesting to learn of the deep_merge approach taken in the answer by "The F". Here is another approach which requires adding a few helper methods.
First, the helper methods:
From the top answer here (converting-a-nested-hash-into-a-flat-hash):
def flat_hash(h,f=[],g={})
return g.update({ f=>h }) unless h.is_a? Hash
h.each { |k,r| flat_hash(r,f+[k],g) }
g
end
From a Github repo called ruby-bury (this functionality was proposed to Ruby core, but rejected)
class Hash
def bury *args
if args.count < 2
raise ArgumentError.new("2 or more arguments required")
elsif args.count == 2
self[args[0]] = args[1]
else
arg = args.shift
self[arg] = {} unless self[arg]
self[arg].bury(*args) unless args.empty?
end
self
end
end
And then a method tying it together:
def change_all_values(hash, &blk)
# the next line makes the method "pure functional"
# but can be removed otherwise.
hash = Marshal.load(Marshal.dump(hash))
flat_hash(hash).each { |k,v| hash.bury(*(k + [blk.call(v)])) }
hash
end
A usage example:
irb(main):063:0> a = {a: 1, b: { c: 1 } }
=> {:a=>1, :b=>{:c=>1}}
irb(main):064:0> b = change_all_values(a) { |val| val + 1 }
=> {:a=>2, :b=>{:c=>2}}
irb(main):066:0> a
=> {:a=>1, :b=>{:c=>1}}
There is deep_merge
yourhash.deep_merge(yourhash) {|_,_,v| v.to_s}
Merge the hash with itself, inspect the value and call to_s on it.
This method requires require 'active_support/core_ext/hash' at the top of file if you are not using ruby on rails.
Obviously, you may handle the conversion of v inside the deep_merge as you like to meet your requirements.
In rails console:
2.3.0 :001 > h1 = { a: true, b: { c: [1, 2, 3] } }
=> {:a=>true, :b=>{:c=>[1, 2, 3]}}
2.3.0 :002 > h1.deep_merge(h1) { |_,_,v| v.to_s}
=> {:a=>"true", :b=>{:c=>"[1, 2, 3]"}}
Well, it's quite simple to write it - so why don't write your own and be absolutely sure how does it behave in all situations ;)
def to_utf8(h)
if h.is_a? String
return h.force_encoding('utf-8')
elsif h.is_a? Symbol
return h.to_s.force_encoding('utf-8').to_sym
elsif h.is_a? Numeric
return h
elsif h.is_a? Array
return h.map { |e| to_utf8(e) }.to_s
else
return h.to_s.force_encoding('utf-8')
end
return hash.to_a.map { |e| result.push(to_utf8(e[0], e[1])) }.to_h
end
You may want to check if all behavior and conversions are correct - and change it if necessary.
I have a rails model that the primary field that the user wants to sort on is a Line Item that is stored in dot-notation format as a string (i.e.: 2.1.4, 2.1.4.1, 2.1.4.5, etc). Ordering alphabetically works great, except that 2.1.4.10 comes before 2.1.4.2 alphabetically. What I want to call 'dot-based numeric order' would put 2.1.4.10 after 2.1.4.9, and 2.4.1.10.1 would precede 2.4.1.11
The question is this: What is The Rails Way™ to set the default order on the model so that the Line Items appear in the correct order, according to 'dot-based numeric order'.
Presume a simple case:
class LineItem < ActiveRecord::Base
validates :line_item, :presence => true, :uniqueness => true
end
and that :line_item is a string.
I assume you are using PostgreSQL and if you really want to set default order for your model, add this default_scope to your LineItem model:
default_scope -> { order("STRING_TO_ARRAY(line_item, '.')::int[] ASC") }
Otherwise I suggest you to use named scope, it can be override and chained:
scope :version_order, -> { order("STRING_TO_ARRAY(line_item, '.')::int[] ASC") }
To do this yourself:
lines = ['3.3.3.3', '3.54.3.3', '3.3.3.20']
sorted = lines.sort do |a, b|
a.split('.').zip(b.split('.')).inject(0) do |res, val|
(res == 0)? val[0].to_i <=> val[1].to_i : res
end
end #=> ["3.3.3.3", "3.3.3.20", "3.54.3.3"]
How it works:
To sort, we pass an array and a block, that blocks gives us 2 arguments that are next to each other in the list, and we can return 0, -1, or 1, which tells Ruby which directions to swap the numbers.
[4,3,-1,2].sort do |x, y|
if x > y
1
elsif x < y
-1
else
0
end
end #=> [-1, 2, 3, 4]
Instead of doing that long logic, Ruby provides a nice operator for us: <=>. Zero means no change, -1 means it's in ascending order, and 1 means the two numbers are in descending order. Ruby repeats that a bunch, and sorts the list.
4 <=> 4 #=> 0
3 <=> 5 #=> -1
5 <=> 3 #=> 1
7 <=> -1 #-> 1
So, we should give higher items (in terms of dots) priority:
#Pseudo Code:
33.44 > 22.55 #=> true
33.44 < 44.33
The easiest way to integrate through all the numbers is an #inject, which gives you a value, and the item you are on. You can do things like this:
[4,4,4].inject(0) {|sum, i| sum + i} #=> 12
[4,4,4].inject(0) {|sum, i| sum - i} #=> -12
['Hello', "I'm penne12"] {|new_word, word| new_word + "-" + word} #=> "Hello-I'm penne12"
So, we'll use an inline if:
(true)? "it's true" : "true is now false. Yay!" #=> "it's true"
(4 > 5)? "logic is weird" : "4 > 5" #=> "4 > 5"
Like this:
.inject(0) do |res, val|
(res == 0)? val[0].to_i <=> val[1].to_i : res
end
We'll split both strings by the ., to get a list:
"Hello. This. Is. A. Test.".split('.') #=> ["Hello", " This", " Is", " A", "Test"]
"4.4.4.4" #=> [4,4,4,4]
And join the two lists together by element using ruby's #Zip (it's really weird.)
[4,4,4,4].zip([5,5,5,5]) #=> [[4,5], [4,5], [4,5], [4,5]]
You can change what item a and b are, if you want to sort by a different property. Ruby doesn't care what you do to either variable, it only cares about the return value.
a, b = a.line_item, b.line_item
On a model:
class LineItem < ActiveRecord::Base
validates :line_item, :presence => true, :uniqueness => true
def self.sort_by_dbno
self.all.sort do |a, b|
a, b = a.line_item, b.line_item
a.split('.').zip(b.split('.')).inject(0) do |res, val|
(res == 0)? val[0].to_i <=> val[1].to_i : res
end
end
end
end
I overrode the <=> operator with #Penne12's code:
def <=>(y)
self.line_item.split('.').zip(y.line_item.split('.')).inject(0) do |res, val|
(res == 0)? val[0].to_i <=> val[1].to_i : res
end
end
Sorting works on any enumerable collection, with no sort block:
bobs_items = LineItem.where(:owner => bob, :complete => false)
"Bob's workload: #{bobs_items.sort.map { |li| li.line_item }.join(', ')}"
I just wrote a method that I'm pretty sure is terribly written. I can't figure out if there is a better way to write this in ruby. It's just a simple loop that is counting stuff.
Of course, I could use a select or something like that, but that would require looping twice on my array. Is there a way to increment several variables by looping without declaring the field before the loop? Something like a multiple select, I don't know. It's even worst when I have more counters.
Thank you!
failed_tests = 0
passed_tests = 0
tests.each do |test|
case test.status
when :failed
failed_tests += 1
when :passed
passed_tests +=1
end
end
You could do something clever like this:
tests.each_with_object(failed: 0, passed: 0) do |test, memo|
memo[test.status] += 1
end
# => { failed: 1, passed: 10 }
You can use the #reduce method:
failed, passed = tests.reduce([0, 0]) do |(failed, passed), test|
case test.status
when :failed
[failed + 1, passed]
when :passed
[failed, passed + 1]
else
[failed, passed]
end
end
Or with a Hash with default value, this will work with any statuses:
tests.reduce(Hash.new(0)) do |counter, test|
counter[test.status] += 1
counter
end
Or even enhancing this with #fivedigit's idea:
tests.each_with_object(Hash.new(0)) do |test, counter|
counter[test.status] += 1
end
Assuming Rails 4 ( using 4.0.x here). I would suggest:
tests.group(:status).count
# -> {'passed' => 2, 'failed' => 34, 'anyotherstatus' => 12}
This will group all records by any possible :status value, and count each individual ocurrence.
Edit: adding a Rails-free approach
Hash[tests.group_by(&:status).map{|k,v| [k,v.size]}]
Group by each element's value.
Map the grouping to an array of [value, counter] pairs.
Turn the array of paris into key-values within a Hash, i.e. accessible via result[1]=2 ....
hash = test.reduce(Hash.new(0)) { |hash,element| hash[element.status] += 1; hash }
this will return a hash with the count of the elements.
ex:
class Test
attr_reader :status
def initialize
#status = ['pass', 'failed'].sample
end
end
array = []
5.times { array.push Test.new }
hash = array.reduce(Hash.new(0)) { |hash,element| hash[element.status] += 1; hash }
=> {"failed"=>3, "pass"=>2}
res_array = tests.map{|test| test.status}
failed_tests = res_array.count :failed
passed_tests = res_array.count :passed
I want to give one of my models an attribute accessor that defaults to an array of eight zeros. This is the first syntax I tried:
attr_accessor_with_default:weekly_magnitude_list, [0,0,0,0,0,0,0,0]
The above didn't do what I expected because all instances of the model end up sharing the same Array object. The blog (http://barelyenough.org/blog/2007/09/things-to-be-suspicious-of-attr_accessor_with_default-with-a-collection/) that clued me into that suggested a different syntax, basically wrapping the default value in a block.
attr_accessor_with_default(:weekly_magnitude_list) {[0,0,0,0,0,0,0,0]}
That doesn't work (for me, in Rails 3). Any time I call the accessor, I seem to be getting a completely new Array object. That effectively means I can't write to it.
Does anybody know the correct way to do this?
For your pleasure, I've included the output of a simple test demonstrating this:
class Container
attr_accessor_with_default :naive_collection, [0,0]
attr_accessor_with_default(:block_collection) {[0,0]}
end
> c = Container.new
=> #<Container:0x7f3610f717a8>
> c.naive_collection[0] = "foo"
=> "foo"
> Container.new.naive_collection
=> ["foo", 0]
# expected [0,0]
> c.block_collection[0] = "foo"
=> "foo"
> c.block_collection
=> [0, 0]
# expected ["foo", 0]
I just stumbled onto this question while running into the same problem.
For reference, the docs specify the block form is dynamically evaluated in the instance scope. To continue the example, its usefulness is really quite limited, but at least works the way you might expect:
class Container
attr_accessor_with_default(:block_collection) { name.underscore }
end
> c = Container.new(:name => "TestName")
> c.block_collection # => "test_name"
> c.block_collection = "something else" # => "something else"
> c.name => "TestName"
Here's the really quirky part though...
class Container
attr_accessor_with_default :naive_collection, [0, 0]
end
# This works as expected
> c = Container.new
> c.naive_collection = ["foo", "bar"] # => ["foo", "bar"]
> Container.new.naive_collection # => [0, 0]
> c.naive_collection[0] = 0 # => [0, "bar"]
> Container.new.naive_collection # => [0, 0]
# But this doesn't
> c2 = Container.new
> c2.naive_collection # => [0, 0]
> c2.naive_collection[0] = "problem!" # => ["problem!", 0]
> Container.new.naive_collection # => ["problem!", 0]
Digging into the source a bit, I see that attr_accessor_with_default defines an instance method that returns the default value or executes the block. Which is fine.
It then goes on to execute this within a module_eval:
def #{sym}=(value)
class << self;
attr_reader :#{sym}
end
##{sym} = value
end
Which is just ridiculously convoluted. I may end up ticketing this as a bug if I can't figure out the motivation for this behavior.
Update:
I managed to figure out what was going wrong here.
def attr_accessor_with_default(sym, default = Proc.new)
define_method(sym, block_given? ? default : Proc.new { default })
module_eval(<<-EVAL, __FILE__, __LINE__ + 1)
def #{sym}=(value)
class << self; attr_accessor :#{sym} end
##{sym} = value
end
EVAL
end
Initially, the default value exists as a proc.
Once you invoke the setter, the getter and setter methods are overwritten by attr_accessor methods and the instance variable is initialized.
The problem is that the default proc in the getter returns the class-level default value. So when you do something like:
> c2.naive_collection[0] = "problem!" # => ["problem!", 0]
you're actually changing the default value for the class.
I think this method should probably be implemented as:
class Module
def attr_accessor_with_default(sym, default = Proc.new)
module_eval(<<-EVAL, __FILE__, __LINE__ + 1)
def #{sym}
class << self; attr_reader :#{sym} end
##{sym} = #{ default.dup }
end
def #{sym}=(value)
class << self; attr_accessor :#{sym} end
##{sym} = value
end
EVAL
end
end
I'll ticket it and offer up a patch.
Update again:
https://rails.lighthouseapp.com/projects/8994-ruby-on-rails/tickets/6496