blob: 1b7b3899c6320408cc26369e6cee18d31f0fd223 [file] [log] [blame]
Brian Silverman8fa2aad2017-06-10 16:45:30 -07001#!/usr/bin/env ruby
2
3# This makes DNS lookups actually work reliably and give useful errors.
4require 'resolv-replace.rb'
5
6require 'yaml'
7require 'optparse'
8
9require './parts'
10require './gschem_file'
11
12=begin
13Property names in the schematic file:
14 value is the main value of the component.
15 secondary_value is another value of the component.
16 The value in the schematic is only a minimum.
17 power rating for resistors.
18 voltage for capacitors.
19 pn_optional set to 1 means that the pn property is non-critical.
20 tolerance is a maximum tolerance for value.
21=end
22
23options = {}
24OptionParser.new do |opts|
25 opts.banner = "Usage: ordering.rb file.sch [options]"
26
27 opts.on('--myro-bom', String, :REQUIRED, 'Generate myropcb BOM') do |v|
28 options[:bom_file] = v
29 options[:bom_style] = :myro
30 end
31 opts.on('--mouser-bom', String, :REQUIRED, 'Generate Mouser BOM (ONLY MOUSER PARTS)') do |v|
32 options[:bom_file] = v
33 options[:bom_style] = :mouser
34 end
35 opts.on('--bom', String, :REQUIRED, 'Generate a nice BOM') do |v|
36 options[:bom_file] = v
37 options[:bom_style] = :nice
38 end
39end.parse!
40
41options[:order_quantity] = 2
42options[:include_dev] = true
43
44if options[:bom_file]
45 $bom_file = File.open(options[:bom_file], 'w')
46 case options[:bom_style]
47 when :myro
48 $bom_file.puts 'Qty,Part Reference,Part Number,MFGR,Distributor NO,DESCRIPTION,FOOTPRINT,DNI'
49 when :mouser
50 $bom_file.puts 'Mfg Part Number,Quantity,Description'
51 else
52 $bom_file.puts 'Quantity,Refdes,Part Number,Footprint,Value,Unit Price'
53 end
54end
55
56if ARGV.size == 0
57 raise 'Need schematics to look at!'
58end
59puts ARGV
60filenames = get_schematic_filenames ARGV
61
Brian Silverman856af592017-12-18 11:17:09 -050062#$parts_yaml = YAML.load_file('parts.yaml')
63$parts_yaml = {}
Brian Silverman8fa2aad2017-06-10 16:45:30 -070064
65module Unicode
66 NBSP = "\u00A0"
67 PlusMinus = "\u00B1"
68 Micro = "\u00B5"
69end
70
71def chomp_zeros s
72 while s[-1] == '0'
73 s.chomp! '0'
74 end
75end
76
77class Part
78 SecondaryValueRegex = /^([0-9.]+) (.*)$/
79
80 attr_reader :refdeses
81 attr_reader :pn, :value, :footprint, :device, :secondary_value, :tolerance
82 attr_reader :pn_optional, :dev_only
83
84 attr_reader :low_quantity
85
86 def quantity
87 @refdeses.size
88 end
89
90 def initialize(pn, value, footprint, device, secondary_value, refdes, tolerance, pn_optional, dev_only)
91 @pn, @value, @footprint, @device = pn, value, footprint, device
92 @secondary_value, @tolerance = secondary_value, tolerance
93 @pn_optional, @dev_only = pn_optional, dev_only
94
95 puts "#{refdes} has no footprint" unless footprint
96
97 @refdeses = [refdes]
98 end
99 def found_another(pn, value, footprint, device, secondary_value, refdes, tolerance, pn_optional, dev_only)
100 error = false
101 if pn != @pn
102 puts "Error: pn #@pn vs #{pn}."
103 error = true
104 end
105 if value != @value
106 puts "Error: value #@value vs #{value}."
107 error = true
108 end
109 if footprint != @footprint
110 puts "Error: footprint #@footprint vs #{footprint}."
111 error = true
112 end
113 if device != @device
114 puts "Error: device #@device vs #{device}."
115 error = true
116 end
117
118 if pn_optional != @pn_optional
119 puts "Error: pn_optional #@pn_optional vs #{pn_optional}."
120 error = true
121 end
122 if dev_only != @dev_only
123 puts "Error: dev_only #@dev_only vs #{dev_only}."
124 error = true
125 end
126
127 if tolerance && @tolerance
128 if tolerance != @tolerance
129 puts "Error: tolerance #@tolerance vs #{tolerance}."
130 error = true
131 end
132 elsif tolerance
133 @tolerance = tolerance
134 end
135
136 new_secondary_match = SecondaryValueRegex.match secondary_value
137 my_secondary_match = SecondaryValueRegex.match @secondary_value
138 if new_secondary_match && my_secondary_match &&
139 new_secondary_match[2] == my_secondary_match[2]
140 new_value = new_secondary_match[1].to_f
141 my_value = my_secondary_match[1].to_f
142 @secondary_value = [new_value, my_value].max.to_s + ' ' + my_secondary_match[2]
143 else
144 if secondary_value != @secondary_value
145 puts "Error: secondary_value #@secondary_value vs #{secondary_value}."
146 error = true
147 end
148 end
149
150 if error
151 puts "\tAdding #{refdes} to #{@refdeses.inspect}"
152 end
153
154 @refdeses.push refdes unless @refdeses.include? refdes
155 end
156
157 def override_pn pn
158 @pn = pn
159 end
160 def override_footprint footprint
161 @footprint = footprint
162 end
163end
164
165$parts = {}
166
167def add_part(attrs, refdes)
168 args = [attrs[:pn], attrs[:value], attrs[:footprint], attrs[:device],
169 attrs[:secondary_value], refdes, attrs[:tolerance],
170 attrs[:pn_optional], attrs[:dev_only]]
171 key = attrs[:pn]
172 unless key
173 key = attrs[:value] || ''
174 key += attrs[:footprint] || ''
175 end
176 raise "No pn or value for #{args} (#{refdes})" unless key && !key.empty?
177 if $parts.include?(key)
178 $parts[key].found_another(*args)
179 else
180 $parts[key] = Part.new(*args)
181 end
182end
183
184def set_or_same(old, new)
185 if new
186 if old
187 if old != new
188 raise "#{old.inspect} != #{new.inspect}"
189 end
190 end
191 new
192 else
193 old
194 end
195end
196
197MouserKeyRegex = /^\n(.*):\n$/
198MouserValueRegex = /^\n(.*)\n$/
199
200def mouser_table_to_map table
201 raise "Bad table from Mouser" if table.empty?
202 r = {}
203 rows = table.css('tr > td > table > tr')
204 rows.each do |row|
205 raise "Unexpected HTML" unless row.elements.length == 3
206
207 name = row.elements[0].text
208 match = MouserKeyRegex.match(name)
209 raise "Unexpected table key #{name.inspect}" unless match
210 key = match[1]
211
212 value = row.elements[1].text
213 match = MouserValueRegex.match(value)
214 raise "Unexpected table value #{value.inspect}" unless match
215 value = match[1]
216 r[key] = value
217 end
218 r
219end
220
221def digikey_table_to_map table
222 raise "Unexpected HTML" unless table.name == 'table'
223 r = {}
224 table.elements.each do |row|
225 raise "Unexpected HTML" unless row.name == 'tr'
226 raise "Unexpected HTML" unless row.elements.length == 2
227
228 name, value = row.elements
229 raise "Unexpected HTML" unless name.name == 'th'
230 raise "Unexpected HTML" unless value.name == 'td'
231 r[name.text.gsub(Unicode::NBSP, '').strip] = value.text.gsub(Unicode::NBSP, '').strip
232 end
233 r
234end
235
236SimpleAttributes = [:pn, :value, :footprint, :device, :tolerance, :pn_optional, :dev_only]
237
238filenames.each do |filename|
239 puts filename
240 file = GschemSchematic.new(filename)
241 file.components.each do |component|
242 next if component.is_power
243 next if component[:graphical]
244
245 simple = {}
246 SimpleAttributes.each do |attr|
247 simple[attr] = component[attr]
248 end
249 case component[:device]
250 when 'CAPACITOR'
251 simple[:secondary_value] = component[:voltage]
252 when 'POLARIZED_CAPACITOR'
253 simple[:secondary_value] = component[:voltage]
254 when 'RESISTOR'
255 simple[:secondary_value] = component[:power]
256 end
257
258 symbol_filename = "schematic/symbols/#{component.filename}"
259 if File.exists? symbol_filename
260 symbol = GschemSymbol.new(symbol_filename)
261 if symbol[:device]
262 if component[:pn]
263 if !component[:footprint]
264 puts "#{component.refdes} has a pn and its symbol has a device but it has no footprint."
265 end
266 else
267 simple[:pn] = symbol[:device]
268 end
269 end
270
271 SimpleAttributes.each do |attr|
272 if symbol[attr]
273 if component[attr]
274 if component[attr] != symbol[attr]
275 puts "#{component.refdes} overriding #{attr} from symbol."
276 puts "\t#{symbol[attr]} => #{component[attr]}"
277 end
278 else
279 simple[attr] = symbol[attr]
280 end
281 end
282 end
283 end
284
285 add_part(simple, component.refdes)
286 end
287end
288
289# An array of arrays where the first one is the English case code and the second
290# is the metric one.
291MetricCases = [['0402', '1005'], ['0603', '1608'], ['0805', '2012'],
292 ['1206', '3216'], ['1210', '3225'], ['1812', '4532']]
293SimpleCaseRegex = /^\d\d\d\d$/
294
295def retrieve_mouser_package attributes
296 raw = attributes['Package / Case']
297 r = nil
298 if raw
299 if SimpleCaseRegex =~ raw
300 r = set_or_same r, raw
301 else
302 t = nil
303 MetricCases.each do |pair|
304 if /^#{pair[0]} \(#{pair[1]} [mM]etric\)$/ =~ raw
305 t = set_or_same r, pair[0]
306 end
307 end
308 r = set_or_same r, t || raw
309 end
310 end
311 r = set_or_same r, attributes['Case Code - in']
312
313 unless r
314 return nil
315 end
316
317 $parts_yaml['footprint_mappings'][r] || r
318end
319
320class PartInfo
321 Price = Struct.new(:quantity, :price, :link, :dpn)
322
323 attr_reader :part
324
325 attr_accessor :case_package
326 attr_accessor :component_value
327 attr_accessor :component_secondary_value
328 attr_accessor :component_value_tolerance
329
330 attr_accessor :order_quantity
331
332 # value_field is the field name for Octopart.
333 # value_unit is the units Octopart has.
334 # value_unit_suffix is the suffix in the .sch file.
335 attr_accessor :value_field, :value_unit, :value_unit_suffix
336 # mouser_value_unit_suffix is the suffix Mouser has on the value.
337 attr_accessor :mouser_value_field, :mouser_value_unit_suffix
338 attr_accessor :digikey_value_field, :digikey_value_unit_suffix
339 attr_accessor :secondary_value_field, :secondary_value_unit
340 attr_accessor :digikey_secondary_value_field, :mouser_secondary_value_field
341 attr_accessor :mouser_secondary_value_field
342
343 attr_accessor :manufacturer
344
345 def initialize part
346 @part = part
347
348 @prices = []
349 @descriptions = []
350 end
351
352 def add_description d
353 @descriptions.push d
354 end
355 def description
356 raise "No description for #{part}" if @descriptions.empty?
357 @descriptions[0]
358 end
359
360 def set_or_same field, value
361 name = '@' + field.to_s
362 old = instance_variable_get name
363 if value
364 if old
365 if value.class == Float && old.class == Float
366 eq = float_eq old, value
367 else
368 eq = old == value
369 end
370 if !eq
371 raise "#{old.inspect} (old) != #{value.inspect} (new) for #{part.inspect}"
372 end
373 else
374 instance_variable_set name, value
375 return value
376 end
377 end
378 return old
379 end
380
381 def add_price price
382 @prices.push price
383 end
384 def price_for quantity
385 return nil, nil if @prices.empty?
386 lowest_info = nil
387 lowest_price = Float::INFINITY
388 @prices.each do |price|
389 c = price.price * (quantity.to_f / price.quantity.to_f).ceil * price.quantity
390 if c < lowest_price
391 lowest_info = price
392 lowest_price = c
393 end
394 end
395 return lowest_price, lowest_info unless lowest_info
396 if lowest_info.quantity.to_f / quantity.to_f > 5 && (lowest_info.quantity - quantity) > 50
397 puts "Warning: no good price for #{quantity} of #{part.inspect}"
398 puts "\thave #{@prices.inspect}"
399 end
400 return lowest_price, lowest_info
401 end
402end
403
404def strip_from_end suffix, raw
405 if suffix.class == Regexp
406 match = suffix.match raw
407 raise "#{raw.inspect} does not match #{suffix}" unless match
408 match[1]
409 else
410 raise "#{raw.inspect} does not end with #{suffix}" unless raw.end_with? suffix
411 raw[0..-(suffix.length + 1)]
412 end
413end
414
415def make_digikey_request part, info
416 pn = $parts_yaml['digikey_numbers'][part.pn] || part.pn
417 page = "http://www.digikey.com/product-search/en?keywords=#{URI.escape pn}"
418 page = $parts_yaml[part.pn]['digikey_url'] || page if $parts_yaml[part.pn]
419 results = Nokogiri::HTML(retrieve_url(page))
420 search_results_html = results.css '#productTable > tbody'
421 values_table_html = results.css '.attributes-table-main'
422 prices_table = results.css '#pricing'
423 product_details = results.css '.product-details'
424 if !results.css(':contains("No records match your search criteria")').empty?
425 # No results from Digikey. Skip it.
426 elsif !values_table_html.empty?
427 raise "Unexpected HTML" unless values_table_html.length == 1
428 raise "Unexpected HTML" unless prices_table.length == 1
429 raise "Unexpected HTML" unless product_details.length == 1
430 parse_digikey_results values_table_html[0].elements[0], prices_table[0],
431 product_details[0], page, info
432 elsif !search_results_html.empty?
433 raise "Unexpected HTML" unless search_results_html.length == 1
434 found = false
435 search_results_html.children.each do |result|
436 if result.elements.length == 0
437 next
438 end
439 raise "Unexpected HTML" unless result.elements.length >= 5
440 links = result.elements[4].css 'a'
441 raise "Unexpected HTML" unless links.length == 1
442 mpn = links[0].text.strip
443 if mpn == part.pn
444 found = true
445
446 actual_page = 'http://www.digikey.com' + links[0]['href']
447 new_results = Nokogiri::HTML(retrieve_url(actual_page))
448 new_values_table = new_results.css '.attributes-table-main'
449 new_prices_table = new_results.css '#pricing'
450 new_product_details = new_results.css '.product-details'
451 new_feedback = results.css '#product-details-feedback'
452 unless new_feedback.empty?
453 if new_feedback.text == 'Obsolete item; call Digi-Key for more information.'
454 next
455 else
456 puts "Warning: Digikey gave feedback #{new_feedback}"
457 end
458 end
459 raise "Unexpected HTML" unless new_values_table.length == 1
460 raise "Unexpected HTML" unless new_prices_table.length == 1
461 raise "Unexpected HTML" unless new_product_details.length == 1
462 parse_digikey_results new_values_table[0].elements[0],
463 new_prices_table[0], new_product_details[0],
464 actual_page, info
465 end
466 end
467
468 if !found
469 puts "Warning: Got multiple Digikey search results for #{part.inspect}, none of them right."
470 end
471 else
472 raise "Don't know what Digikey page #{page} => #{results} for #{part.inspect} contains!"
473 end
474end
475
476def make_mouser_request part, info
477 pn = $parts_yaml['mouser_numbers'][part.pn] || part.pn
478 url = "http://www.mouser.com/Search/Refine.aspx?Keyword=#{URI.escape pn}"
479 mouser_results = Nokogiri::HTML(retrieve_url(url))
480 search_results_html = mouser_results.css '#ctl00_ContentMain_SearchResultsGrid_grid'
481 values_table_html = mouser_results.css '.specs'
482 prices_table = mouser_results.css '.PriceBreaks'
483 details_table = mouser_results.css '#product-desc'
484 if !mouser_results.css(':contains("did not return any results.")').empty?
485 # No results from Mouser. Skip it.
486 elsif !values_table_html.empty?
487 parse_mouser_results values_table_html, prices_table, details_table, url, info
488 elsif !search_results_html.empty?
489 found = false
490 search_results_html.children[2..-1].each do |result|
491 if result.elements.length == 0
492 next
493 end
494 raise "Unexpected HTML" unless result.elements.length >= 4
495 links = result.elements[3].css 'a'
496 raise "Unexpected HTML" unless links.length >= 1
497 mpn = links.first.text.strip
498 if mpn == part.pn
499 if result.elements[2].text.strip == 'Not Assigned'
500 # This means that Mouser doesn't actually carry it and probably
501 # doesn't have much data on it, none of which I want to trust.
502 next
503 end
504 found = true
505
506 actual_page = 'http://www.mouser.com/Search/' + links[0]['href']
507 mouser_results = Nokogiri::HTML(retrieve_url(actual_page))
508 values_table_html = mouser_results.css '.specs'
509 prices_table = mouser_results.css '.PriceBreaks'
510 details_table = mouser_results.css '#product-desc'
511 parse_mouser_results values_table_html, prices_table, details_table, actual_page, info
512 end
513 end
514 if !found
515 puts "Warning: Got multiple Mouser search results for #{part.inspect}, none of them right."
516 end
517 else
518 raise "Don't know what Mouser page #{url} => #{mouser_results} contains!"
519 end
520end
521
522WeirdFractionRegex = /^([0-9.]+ [a-zA-Z]+) \([0-9]+\/[0-9]+ [a-zA-Z]+\)$/
523
524def parse_digikey_results values_table_html, prices_table, product_details, url, info
525 attributes = digikey_table_to_map values_table_html
526
527 digikey_package = retrieve_mouser_package attributes
528 if digikey_package != 'Non Standard' && digikey_package != 'Nonstandard'
529 info.set_or_same :case_package, digikey_package
530 end
531
532 if info.digikey_value_field && attributes[info.digikey_value_field]
533 v = strip_from_end info.digikey_value_unit_suffix, attributes[info.digikey_value_field]
534 info.set_or_same :component_value, to_f_with_suffix(v)
535 end
536 if info.digikey_secondary_value_field && attributes[info.digikey_secondary_value_field]
537 v = strip_from_end info.secondary_value_unit, attributes[info.digikey_secondary_value_field]
538 info.set_or_same :component_secondary_value, to_f_with_suffix(v)
539 end
540
541 if attributes['Tolerance']
542 if attributes['Tolerance'] == '-20%, +80%'
543 info.set_or_same :component_value_tolerance, 80
544 else
545 v = strip_from_end '%', attributes['Tolerance'].gsub(Unicode::PlusMinus, '')
546 info.set_or_same :component_value_tolerance, v.to_f
547 end
548 end
549
550 distributor_row = product_details.elements[2]
551 raise "Unexpected HTML" unless distributor_row.elements[0].text == 'Digi-Key Part Number'
552 description_row = product_details.elements[6]
553 raise "Unexpected HTML" unless description_row.elements[0].text == 'Description'
554
555 info.add_description description_row.elements[1].text.chomp
556
557 prices_table.elements[1..-1].each do |price|
558 quantity = price.elements[0].text.gsub(',', '').to_i
559 raise unless quantity.to_s == price.elements[0].text.gsub(',', '')
560 price_number = price.elements[1].text.gsub(',', '').to_f
561 raise unless chomp_zeros(price_number.to_s) == chomp_zeros(price.elements[1].text)
562 info.add_price PartInfo::Price.new(quantity, price_number, url,
563 distributor_row.elements[1].text)
564 end
565
566 manufacturer_row = product_details.elements[4]
567 raise "Unexpected HTML" unless manufacturer_row.elements[0].text == 'Manufacturer'
568 manufacturer = manufacturer_row.elements[1].text
569 manufacturer = $parts_yaml['manufacturer_names'][manufacturer] || manufacturer
570 info.set_or_same :manufacturer, manufacturer
571end
572
573def parse_mouser_results values_table_html, prices_table, details_table, url, info
574 attributes = mouser_table_to_map values_table_html
575
576 mouser_package = retrieve_mouser_package attributes
577 if info.case_package == 'SRR1210' && mouser_package == '1210'
578 elsif info.case_package == '0612' && mouser_package == '1206'
579 else
580 info.set_or_same :case_package, mouser_package
581 end
582
583 if info.mouser_value_field && attributes[info.mouser_value_field]
584 v = strip_from_end info.mouser_value_unit_suffix, attributes[info.mouser_value_field]
585 info.set_or_same :component_value, to_f_with_suffix(v)
586 end
587 if info.mouser_secondary_value_field && attributes[info.mouser_secondary_value_field]
588 v = attributes[info.mouser_secondary_value_field]
589 match = WeirdFractionRegex.match v
590 if match
591 v = match[1]
592 end
593 v = strip_from_end info.secondary_value_unit, v
594 info.set_or_same :component_secondary_value, to_f_with_suffix(v)
595 end
596
597 if info.part.device != 'CRYSTAL' && attributes['Tolerance']
598 v = strip_from_end '%', attributes['Tolerance'].gsub('+/-', '')
599 info.set_or_same :component_value_tolerance, v.to_f
600 end
601
602 raise "Unexpected HTML" unless details_table.length == 1
603 details_table_contents = {}
604 if details_table[0].elements.size > 1
605 details_table.first.elements.each do |row|
606 details_table_contents[row.elements[0].text.strip] =
607 row.elements[1].text.strip
608 end
609 else
610 details_table.first.elements.first.elements.each do |row|
611 details_table_contents[row.elements.first.text.strip] =
612 row.elements[1].text.strip
613 end
614 end
615 details_table_contents.each do |_, v|
616 if v == 'Obsolete'
617 return
618 end
619 end
620
621 manufacturers = details_table_contents['Manufacturer:'].lines.collect do |line|
622 line.strip
623 end.delete_if do |line|
624 line.empty?
625 end.uniq
626 unless manufacturers.length == 1
627 raise "Not sure what manufacturers #{manufactures.inspect} from Mouser mean"
628 end
629 manufacturer = manufacturers[0]
630 manufacturer = $parts_yaml['manufacturer_names'][manufacturer] || manufacturer
631 info.set_or_same :manufacturer, manufacturer
632
633 distributor = details_table_contents['Mouser Part #:']
634 if distributor == 'Not Assigned'
635 return
636 end
637 raise "Unexpected HTML" unless prices_table.length == 1
638
639 info.add_description details_table_contents['Description:'].chomp
640
641 prices_table[0].elements.each do |row|
642 raise "Unexpected HTML" unless row.name == 'div' || row.name == 'tr'
643 if row.elements.length == 4
644 quantity_text = row.elements[1].elements[0].text
645 price_text = row.elements[2].text.strip
646 if price_text == 'Quote>'
647 next
648 elsif price_text == ''
649 # There is (sometimes?) an empty row at the bottom.
650 raise "Huh?" unless quantity_text == ''
651 next
652 elsif !price_text.start_with? '$'
653 puts row
654 raise "Don't know how to interpret Mouser price #{price_text.inspect}."
655 end
656 quantity = quantity_text.gsub(',', '').to_i
657 raise unless quantity.to_s == quantity_text.gsub(',', '')
658 price = price_text[1..-1].gsub(',', '').to_f
659 raise unless chomp_zeros(price.to_s) == chomp_zeros(price_text[1..-1])
660 info.add_price PartInfo::Price.new(quantity, price, url, distributor)
661 end
662 end
663end
664
665def parse_value f, unit
666 raise "Don't know what min/max values mean." if f['min_value'] || f['max_value']
667 v = f['value'][0]
668 if unit == '%'
669 raise "Want %, not units" if f['metadata']['unit']
670 if v == '+80/-20%'
671 v = '80'
672 else
673 v = strip_from_end '%', v.gsub(Unicode::PlusMinus, '')
674 end
675 else
676 if f['metadata']['unit']['name'] != unit
677 raise "Wrong units on #{f.inspect} (expected #{unit})."
678 end
679 end
680 raise "Don't know what multiple values #{f['value'].uniq} mean." if f['value'].uniq.size != 1
681 r = v.to_f
682 raise "Error parsing #{v}" if r == 0.0
683 r
684end
685
686SiSuffixes = {'M' => 1e6, 'k' => 1e3, 'm' => 1e-3, 'u' => 1e-6, 'n' => 1e-9,
687 'p' => 1e-12, 'f' => 1e-15,
688 Unicode::Micro => 1e-6}
689
690# Matches Digikey-style resistor power ratings like "0.063W, 1/16".
691FractionalWattsRegex = /^([0-9.]+)W, ([0-9]+)\/([0-9]+)$/
692
693def to_f_with_suffix raw
694 raw.rstrip!
695 suffix_power = 1
696 SiSuffixes.each do |suffix, power|
697 if raw.end_with? suffix
698 raw = raw[0..-2]
699 suffix_power *= power
700 end
701 end
702 raw.rstrip!
703
704 fractional_watts = FractionalWattsRegex.match(raw)
705 if fractional_watts
706 raise "Fraction and power for #{raw.inspect}?" unless suffix_power == 1
707 decimal = fractional_watts[1].to_f
708 fraction = fractional_watts[2].to_f / fractional_watts[3].to_f
709 if (decimal - fraction).abs > 10 ** -(fractional_watts[1].length - 2)
710 raise "Mismatched fraction #{fraction} and decimal #{decimal}"
711 end
712 return decimal
713 end
714
715 raise "Can't parse number #{raw.inspect}" if /^[0-9.]+$/ !~ raw
716
717 raw.to_f * suffix_power
718end
719
720def float_eq a, b
721 ((a - b).abs / [a.abs, b.abs].max) < 1e-15
722end
723
724def check_value_attribute raw, suffix, expected
725 raw = strip_from_end suffix, raw
726 found_value = to_f_with_suffix raw
727 float_eq expected, found_value
728end
729
730total_price = 0
731total_per_item_price = 0
732
733$parts.each do |_, part|
734 info = PartInfo.new part
735
736 unit_price = nil
737 order_quantity = nil
738 if $bom_file
Brian Silverman856af592017-12-18 11:17:09 -0500739 #footprint = $parts_yaml['bom_footprints'][part.footprint] || part.footprint
740 footprint = part.footprint
Brian Silverman8fa2aad2017-06-10 16:45:30 -0700741 case options[:bom_style]
742 when :myro
743 line = [part.quantity,
744 part.refdeses.join(' '),
745 part.pn,
746 info.manufacturer,
747 lowest_price_info ? lowest_price_info.dpn : 'NA',
748 info.description.gsub('"', ''),
749 footprint,
750 dnp ? 'DNI' : ''].collect do |o|
751 o.to_s.inspect
752 end.join(',')
753 when :mouser
754 if lowest_price_info.link.include?('mouser.com')
755 #line = "#{part.pn.inspect},#{order_quantity},#{info.description.gsub('"', '').inspect}"
756 elsif lowest_price_info.link.include?('digikey.com')
757 line = "#{lowest_price_info.dpn},#{order_quantity}"
758 else
759 raise
760 end
761 else
762 line = [part.quantity,
763 part.refdeses.join(' '),
764 part.pn,
765 footprint,
766 part.value,
767 unit_price].collect do |o|
768 o.to_s.inspect
769 end.join(',')
770 end
771 $bom_file.puts line if line
772 end
773end
774puts total_price
775puts total_per_item_price
776
777$bom_file.close if $bom_file