How to ask and validate a length from a `UI.Inputbox` field? - ruby

On SketchUp one can request user input using a dialog window created with the UI.Inputbox method. It is a basic task to ask for the dimension or length of some geometry the script will create afterwards.
Internally SketchUp uses Inches to define geometry. The user will give the answer in his/her localized dimension idiom: '1,5m' for 1.5 Meters. The built in SketchUp method .to_l converts strings to length. See https://github.com/thomthom/SketchUp-Units-and-Locale-Helper#sketchups-shortcomings for reference.
I'm asking the user for a length as a string from UI.Inputbox and then converting it to a length value using .to_l but if the user types an invalid value I don't know how check or how to handle the error.
As my localized length inputs have ',' as the decimal separation (in Portuguese it is 1,5m not 1.5m), I'm not willing to ask for a floating point value.
prompts = ['Length']
defaults = ['1,2']
inputs = UI.inputbox( prompts, defaults ,'Length Input')
inputs[0].to_l
#try inputs[0].to_f
How check the input string or catch .to_l failure?

This should work for you, but it might need to be fine-tuned depending on what you want each error case to do.
prompts = ['Length']
defaults = ['1,2']
inputs = UI.inputbox( prompts, defaults ,'Length Input')
Assuming the above, we can make a method to handle error cases for your input.
def parse_input(inputs)
input = normalize(inputs)
handle_error if input.empty?
input
end
def normalize(input)
input.gsub(/[a-zA-Z]/, "")
end
def handle_error
# you could raise an error or just return the default
end
When you parse the input, it attempts to 'normalize' it - meaning that it replaces commas with dots and then calls to_f. Since it looks like you're getting this as input from a field, inputs will always be a String, so there's no need to worry about handling other types. Sample output:
normalize('1,4') #=> '1.4'
normalize('2.6') #=> '2.6'
normalize('5a.2') #=> '5.2'
normalize('text') #=> ''
Now that the input is normalized, you can handle errors (which is basically when the normalized input is an empty string. What you want to do there is up to you and what works best in your specific case.
Finally the input is returned if there are no input errors.

I'm asking the user for a length as a string from UI.Inputbox and then
converting it to a length value using .to_l
The question is wrong, I mean, it is not a good idea to ask for a string and to try yourself to manage the complexities of localized float values. Don't do it!
Aks for a 'length' and SketchUp will take care of everything for you. Use .m and the user will be prompted a value in his local/chosen units. The resulting input will be in 'Length - internal units'.
prompts = ['Name', 'Width', 'Height']
defaults = ['My Own Square', 5.m, 2.m]
input = UI.inputbox( prompts, defaults, 'Create Square' )
# User enters Width: 300cm, Height 4
p input
# => ["My Own Square", 118.110236220472, 157.48031496063]
p input.map { |n| n.class }
# => [String, Length, Length]
p input.map { |n| n.to_s }
# => ["My Own Square", "3000mm", "4000mm"]
Source: ThomThom http://www.thomthom.net/thoughts/2012/08/dealing-with-units-in-sketchup/

Related

Ruby implicit conversion of String into Integer (typeError)

I am trying to use a YAML file, reading from it and writing to it a list of values. On the first run of this script, the yaml file is correctly created, but then on the second it throws a conversion TypeError which I don't know to fix.
db_yml = 'store.yml'
require 'psych'
begin
if File.exist?(db_yml)
yml = Psych.load_file(db_yml)
puts "done load"
yml['reminders']['reminder_a'] = [123,456]
yml['reminders']['reminder_b'] = [457,635,123]
File.write(db_yml, Psych.dump(yml) )
else
#the file does not exist yet, create an empty one.
File.write(db_yml, Psych.dump(
{'reminders' => [
{'reminder_a'=> [nil]},
{'reminder_b'=> [nil]}
]}
)) #Store
end
rescue IOError => msg
# display the system generated error message
puts msg
end
produces the file store.yml on first run:
---
reminders:
- reminder_a:
-
- reminder_b:
-
So far so good. But then on the second run it fails with
done load
yamlstore.rb:23:in `[]=': no implicit conversion of String into Integer (TypeError)
from yamlstore.rb:23:in `<main>'
Could you tell me where I am going wrong?
The error message says that you were passing a String where Ruby expects something that is implicitly convertible to an Integer. The number one place where Ruby expects something that is implicitly convertible to an Integer is when indexing into an Array. So, whenever you see this error message, you can be 99% sure that you are either indexing an Array with something you thought was an Integer but isn't, or that you are indexing an Array that you thought was something else (most likely a Hash). (The other possibility is that you are trying to do arithmetic with a mix of Integers and Strings.)
Just because Ruby is a dynamically-typed programming language does not mean that you don't need to care about types. In particular, YAML is a (somewhat) typed serialization format.
The type of the file you are creating looks something like this:
Map<String, Sequence<Map<String, Sequence<Int | null>>>>
However, you are accessing it, as if it were typed like this:
Map<String, Map<String, Sequence<Int | null>>>
To put it more concretely, you are creating the value corresponding to the key 'reminders' as a sequence (in YAML terms, an Array in Ruby terms) of maps (Hashes). Arrays are indexed by Integers.
You, however, are indexing it with a String, as if it were a Hash.
So, you either need to change how you access the values like this:
yml['reminders'][0]['reminder_a'] = [123, 456]
# ↑↑↑
yml['reminders'][1]['reminder_b'] = [457,635,123]
# ↑↑↑
Or change the way you initialize the file like this:
File.write(db_yml, Psych.dump(
{ 'reminders' => {
# ↑
'reminder_a' => [nil],
# ↑ ↑
'reminder_b' => [nil]
# ↑ ↑
}
so that the resulting YAML document looks like this:
---
reminders:
reminder_a:
-
reminder_b:
-
There is nothing wrong with the YAML file. However you create the file you create it with the following structure:
yaml = {
'reminders' => [
{'reminder_a'=> [nil]},
{'reminder_b'=> [nil]}
]
}
Notice that the contents of yaml['reminders'] is an array. Where it goes wrong is here:
reminders = yaml['reminders']
reminder_a = reminders['reminder_a'] # <= error
# in the question example:
# yml['reminders']['reminder_a'] = [123,456]
Since reminders is an array you can't access it by passing a string as index. You have 2 options:
In my opinion the best option (if you want to access the reminders by key) is changing the structure to use a hash instead of an array:
yaml = {
'reminders' => {
'reminder_a'=> [nil],
'reminder_b'=> [nil]
}
}
With the above structure you can access your reminder through:
yaml['reminders']['reminder_a']
Somewhat clumsy, find the array element with the correct key:
yaml['reminders'].each do |reminder|
reminder['reminder_a'] = [123,456] if reminder.key? 'reminder_a'
reminder['reminder_b'] = [457,635,123] if reminder.key? 'reminder_b'
end

How do I convert a spreadsheet "letternamed" column coordinate to an integer?

In spreadsheets I have cells named like "F14", "BE5" or "ALL1". I have the first part, the column coordinate, in a variable and I want to convert it to a 0-based integer column index.
How do I do it, preferably in an elegant way, in Ruby?
I can do it using a brute-force method: I can imagine loopping through all letters, converting them to ASCII and adding to a result, but I feel there should be something more elegant/straightforward.
Edit: Example: To simplify I do only speak about the column coordinate (letters). Therefore in the first case (F14) I have "F" as the input and I expect the result to be 5. In the second case I have "BE" as input and I expect getting 56, for "ALL" I want to get 999.
Not sure if this is any clearer than the code you already have, but it does have the advantage of handling an arbitrary number of letters:
class String
def upcase_letters
self.upcase.split(//)
end
end
module Enumerable
def reverse_with_index
self.map.with_index.to_a.reverse
end
def sum
self.reduce(0, :+)
end
end
def indexFromColumnName(column_str)
start = 'A'.ord - 1
column_str.upcase_letters.map do |c|
c.ord - start
end.reverse_with_index.map do |value, digit_position|
value * (26 ** digit_position)
end.sum - 1
end
I've added some methods to String and Enumerable because I thought it made the code more readable, but you could inline these or define them elsewhere if you don't like that sort of thing.
We can use modulo and the length of the input. The last character will
be used to calculate the exact "position", and the remainders to count
how many "laps" we did in the alphabet, e.g.
def column_to_integer(column_name)
letters = /[A-Z]+/.match(column_name).to_s.split("")
laps = (letters.length - 1) * 26
position = ((letters.last.ord - 'A'.ord) % 26)
laps + position
end
Using decimal representation (ord) and the math tricks seems a neat
solution at first, but it has some pain points regarding the
implementation. We have magic numbers, 26, and constants 'A'.ord all
over.
One solution is to give our code better knowlegde about our domain, i.e.
the alphabet. In that case, we can switch the modulo with the position of
the last character in the alphabet (because it's already sorted in a zero-based array), e.g.
ALPHABET = ('A'..'Z').to_a
def column_to_integer(column_name)
letters = /[A-Z]+/.match(column_name).to_s.split("")
laps = (letters.length - 1) * ALPHABET.size
position = ALPHABET.index(letters.last)
laps + position
end
The final result:
> column_to_integer('F5')
=> 5
> column_to_integer('AK14')
=> 36
HTH. Best!
I have found particularly neat way to do this conversion:
def index_from_column_name(colname)
s=colname.size
(colname.to_i(36)-(36**s-1).div(3.5)).to_s(36).to_i(26)+(26**s-1)/25-1
end
Explanation why it works
(warning spoiler ;) ahead). Basically we are doing this
(colname.to_i(36)-('A'*colname.size).to_i(36)).to_s(36).to_i(26)+('1'*colname.size).to_i(26)-1
which in plain English means, that we are interpreting colname as 26-base number. Before we can do it we need to interpret all A's as 1, B's as 2 etc. If only this is needed than it would be even simpler, namely
(colname.to_i(36) - '9'*colname.size).to_i(36)).to_s(36).to_i(26)-1
unfortunately there are Z characters present which would need to be interpreted as 10(base 26) so we need a little trick. We shift every digit 1 more then needed and than add it at the end (to every digit in original colname)
`

refactor this ruby database/sequel gem lookup

I know this code is not optimal, any ideas on how to improve it?
job_and_cost_code_found = false
timberline_db['SELECT Job, Cost_Code FROM [JCM_MASTER__COST_CODE] WHERE [Job] = ? AND [Cost_Code] = ?', job, clean_cost_code].each do |row|
job_and_cost_code_found = true
end
if job_and_cost_code_found == false then
info = linenum + "," + id + ",,Employees default job and cost code do not exist in timberline. job:#{job} cost code:#{clean_cost_code}"
add_to_exception_output_file(info)
end
You're breaking a lot of simple rules here.
Don't select what you don't use.
You select a number of columns, then completely ignore the result data. What you probably want is a count:
SELECT COUNT(*) AS cost_code_count FROM [JCM_MASTER__COST_CODE] WHERE [Job] = ? AND [Cost_Code] = ?'
Then you'll get one row that will have either a zero or non-zero value in it. Save this into a variable like:
job_and_cost_codes_found = timberline_db[...][0]['cost_code_count']
Don't compare against false unless you need to differentiate between that and nil
In Ruby only two things evaluate as false, nil and false. Most of the time you will not be concerned about the difference. On rare occasions you might want to have different logic for set true, set false or not set (nil), and only then would you test so specifically.
However, keep in mind that 0 is not a false value, so you will need to compare against that.
Taking into account the previous optimization, your if could be:
if job_and_cost_codes_found == 0
# ...
end
Don't use then or other bits of redundant syntax
Most Ruby style-guides spurn useless syntax like then, just as they recommend avoiding for and instead use the Enumerable class which is far more flexible.
Manipulate data, not strings
You're assembling some kind of CSV-like line in the end there. Ideally you'd be using the built-in CSV library to do the correct encoding, and libraries like that want data, not a string they'd have to parse.
One step closer to that is this:
line = [
linenum,
id,
nil,
"Employees default job and cost code do not exist in timberline. job:#{job} cost code:#{clean_cost_code}"
].join(',')
add_to_exception_output_file(line)
You'd presumably replace join(',') with the proper CSV encoding method that applies here. The library is more efficient when you can compile all of the data ahead of time into an array-of-arrays, so I'd recommend doing that if this is the end goal.
For example:
lines = [ ]
# ...
if (...)
# Append an array to the lines to write to the CSV file.
lines << [ ... ]
end
Keep your data in a standard structure like an Array, a Hash, or a custom object, until you're prepared to commit it to its final formatted or encoded form. That way you can perform additional operations on it if you need to do things like filtering.
It's hard to refactor this when I'm not exactly sure what it's supposed to be doing, but assuming that you want to log an error when there's no entry matching a job & code pair, here's what I've come up with:
def fetch_by_job_and_cost_code(job, cost_code)
timberline_db['SELECT Job, Cost_Code FROM [JCM_MASTER__COST_CODE] WHERE [Job] = ? AND [Cost_Code] = ?', job, cost_code]
end
if fetch_by_job_and_cost_code(job, clean_cost_code).none?
add_to_exception_output_file "#{linenum},#{id},,Employees default job and cost code do not exist in timberline. job:#{job} cost code:#{clean_cost_code}"
end

What is the pythonic way to print values right aligned?

I've a list of strings which I want to group by their suffix and then print the values right-aligned, padding the left side with spaces.
What is the pythonic way to do that?
My current code is:
def find_pos(needle, haystack):
for i, v in enumerate(haystack):
if str(needle).endswith(v):
return i
return -1
# Show only Error and Warning things
search_terms = "Error", "Warning"
errors_list = filter(lambda item: str(item).endswith(search_terms), dir(__builtins__))
# alphabetical sort
errors_list.sort()
# Sort the list so Errors come before Warnings
errors_list.sort(lambda x, y: find_pos(x, search_terms) - find_pos(y, search_terms))
# Format for right-aligning the string
size = str(len(max(errors_list, key=len)))
fmt = "{:>" + size + "s}"
for item in errors_list:
print fmt.format(item)
An alternative I had in mind was:
size = len(max(errors_list, key=len))
for item in errors_list:
print str.rjust(item, size)
I'm still learning Python, so other suggestions about improving the code is welcome too.
Very close.
fmt = "{:>{size}s}"
for item in errors_list:
print fmt.format(item, size=size)
The two sorting steps can be combined into one:
errors_list.sort(key=lambda x: (x, find_pos(x, search_terms)))
Generally, using the key parameter is preferred over using cmp. Documentation on sorting
If you are interested in the length anyway, using the key parameter to max() is a bit pointless. I'd go for
width = max(map(len, errors_list))
Since the length does not change inside the loop, I'd prepare the format string only once:
right_align = ">{}".format(width)
Inside the loop, you can now do with the free format() function (i.e. not the str method, but the built-in function):
for item in errors_list:
print format(item, right_align)
str.rjust(item, size) is usually and preferrably written as item.rjust(size).
You might want to look here, which describes how to right-justify using str.rjust and using print formatting.

Calculating the size of an Array pack struct format in Ruby

In the case of e.g. ddddd, d is the native format for the system, so I can't know exactly how big it will be.
In python I can do:
import struct
print struct.calcsize('ddddd')
Which will return 40.
How do I get this in Ruby?
I haven't found a built-in way to do this, but I've had success with this small function when I know I'm dealing with only numeric formats:
def calculate_size(format)
# Only for numeric formats, String formats will raise a TypeError
elements = 0
format.each_char do |c|
if c =~ /\d/
elements += c.to_i - 1
else
elements += 1
end
end
([ 0 ] * elements).pack(format).length
end
This constructs an array of the proper number of zeros, calls pack() with your format, and returns the length (in bytes). Zeros work in this case because they're convertible to each of the numeric formats (integer, double, float, etc).
I don't know of a shortcut but you can just pack one and ask how long it is:
length_of_five_packed_doubles = 5 * [1.0].pack('d').length
By the way, a ruby array combined with the pack method appears to be functionally equivalent to python's struct module. Ruby pretty much copied perl's pack and put them as methods on the Array class.

Resources