classifieds_cli_app 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile.lock +4 -4
- data/lib/classifieds/auto.rb +3 -9
- data/lib/classifieds/auto_scraper.rb +1 -1
- data/lib/classifieds/boat.rb +3 -9
- data/lib/classifieds/boat_scraper.rb +5 -5
- data/lib/classifieds/item.rb +56 -31
- data/lib/classifieds/listing.rb +9 -9
- data/lib/classifieds/seller.rb +3 -3
- data/lib/classifieds/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA1:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 01be9206924ea8e879c3236c148e9c2d6a339ae5
|
|
4
|
+
data.tar.gz: 3588d77e9d44be47a69d3fb05ecf11da97624cd7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d499692bde2dc07c237c0e4ac450755d848e7fcf935e0e9b8d962bae5c3c6114f87a6e9f41b08b11b08d97909e4316abd835acc4e956f556107e3d2ebe425c40
|
|
7
|
+
data.tar.gz: 158f1ba26b7b52697104b73e80203782849ef4658177687b618f42ce464aa39bfde9a55a975e6be66ca967ba83586e0b35c69d4a5d33683ed1bd6eac15cad71c
|
data/Gemfile.lock
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
|
|
5
|
-
nokogiri
|
|
4
|
+
classifieds_cli_app (0.1.1)
|
|
5
|
+
nokogiri (~> 1.6)
|
|
6
6
|
|
|
7
7
|
GEM
|
|
8
8
|
remote: https://rubygems.org/
|
|
@@ -24,8 +24,8 @@ PLATFORMS
|
|
|
24
24
|
|
|
25
25
|
DEPENDENCIES
|
|
26
26
|
bundler (~> 1.13)
|
|
27
|
-
|
|
28
|
-
pry
|
|
27
|
+
classifieds_cli_app!
|
|
28
|
+
pry (~> 0)
|
|
29
29
|
rake (~> 10.0)
|
|
30
30
|
|
|
31
31
|
BUNDLED WITH
|
data/lib/classifieds/auto.rb
CHANGED
|
@@ -1,18 +1,12 @@
|
|
|
1
1
|
class Classifieds::Auto < Classifieds::Vehicle # describes a Vehicle type of: Automobile
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
@@SUMMARY_COL_FORMATS = [[24,'l'], [7,'r'], [8,'r']] # [width, justification]
|
|
3
|
+
SUMMARY_COL_FORMATS = [[32,'l'], [7,'r'], [9,'r']] # [width, justification]
|
|
5
4
|
|
|
6
5
|
def initialize(year, make, model, mileage, price, condition, detail_link)
|
|
7
6
|
super(year, make, model, price, condition, detail_link)
|
|
8
7
|
@mileage = mileage
|
|
9
8
|
end
|
|
10
9
|
|
|
11
|
-
# Return attribute field width for given column
|
|
12
|
-
def attr_width(col)
|
|
13
|
-
@@ATTR_COLUMN_WIDTHS[col-1]
|
|
14
|
-
end
|
|
15
|
-
|
|
16
10
|
# Creates listings from summary web page
|
|
17
11
|
def self.scrape_results_page(results_url, results_url_file, results_doc)
|
|
18
12
|
Classifieds::AutoScraper.scrape_results_page(results_url, results_url_file, results_doc, self)
|
|
@@ -25,11 +19,11 @@ class Classifieds::Auto < Classifieds::Vehicle # describes a Vehicle type of: A
|
|
|
25
19
|
|
|
26
20
|
# Returns a summary listing data row
|
|
27
21
|
def summary_detail
|
|
28
|
-
Classifieds::Listing.
|
|
22
|
+
Classifieds::Listing.format_cols([@title, @mileage, @price], SUMMARY_COL_FORMATS)
|
|
29
23
|
end
|
|
30
24
|
|
|
31
25
|
# Returns the summary listing title row
|
|
32
26
|
def self.summary_header
|
|
33
|
-
Classifieds::Listing.
|
|
27
|
+
Classifieds::Listing.format_cols(['Vehicle', 'Mileage', 'Price '], SUMMARY_COL_FORMATS)
|
|
34
28
|
end
|
|
35
29
|
end
|
|
@@ -31,7 +31,7 @@ class Classifieds::AutoScraper # converts automobile classified listings into o
|
|
|
31
31
|
# Returns detail attributes and values in detail_values hash
|
|
32
32
|
def self.scrape_results_detail_page(detail_doc, item_condition, detail_values)
|
|
33
33
|
# Create some entries manually.
|
|
34
|
-
detail_values['Description'.to_sym] = detail_doc.css('.aiDetailsDescription')[0].children[2].text.strip
|
|
34
|
+
detail_values['Description'.to_sym] = detail_doc.css('.aiDetailsDescription')[0].children[2].text.strip # Description must be first attribute.
|
|
35
35
|
detail_values['Condition'.to_sym] = item_condition
|
|
36
36
|
detail_values['Certified'.to_sym] = ''
|
|
37
37
|
# Create the rest from scraping the html's detail attrribute/value table.
|
data/lib/classifieds/boat.rb
CHANGED
|
@@ -1,17 +1,11 @@
|
|
|
1
1
|
class Classifieds::Boat < Classifieds::Vehicle # describes a Vehicle type of: Boat
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
@@SUMMARY_COL_FORMATS = [[24,'l'], [8,'r']] # [width, justification]
|
|
3
|
+
SUMMARY_COL_FORMATS = [[32,'l'], [9,'r']] # [width, justification]
|
|
5
4
|
|
|
6
5
|
def initialize(year, make, model, price, condition, detail_link)
|
|
7
6
|
super(year, make, model, price, condition, detail_link)
|
|
8
7
|
end
|
|
9
8
|
|
|
10
|
-
# Return attribute field width for given column
|
|
11
|
-
def attr_width(col)
|
|
12
|
-
@@ATTR_COLUMN_WIDTHS[col-1]
|
|
13
|
-
end
|
|
14
|
-
|
|
15
9
|
# Creates listings from summary web page
|
|
16
10
|
def self.scrape_results_page(results_url, results_url_file, results_doc)
|
|
17
11
|
Classifieds::BoatScraper.scrape_results_page(results_url, results_url_file, results_doc, self)
|
|
@@ -24,11 +18,11 @@ class Classifieds::Boat < Classifieds::Vehicle # describes a Vehicle type of: B
|
|
|
24
18
|
|
|
25
19
|
# Returns a summary listing data row
|
|
26
20
|
def summary_detail
|
|
27
|
-
Classifieds::Listing.
|
|
21
|
+
Classifieds::Listing.format_cols([@title, @price], SUMMARY_COL_FORMATS)
|
|
28
22
|
end
|
|
29
23
|
|
|
30
24
|
# Returns the summary listing title row
|
|
31
25
|
def self.summary_header
|
|
32
|
-
Classifieds::Listing.
|
|
26
|
+
Classifieds::Listing.format_cols(['Boat', 'Price '], SUMMARY_COL_FORMATS)
|
|
33
27
|
end
|
|
34
28
|
end
|
|
@@ -60,7 +60,7 @@ private
|
|
|
60
60
|
def self.do_alt_processing(doc, item_condition, detail_values)
|
|
61
61
|
# Create some entries manually.
|
|
62
62
|
main_content = doc.css('#main-content')
|
|
63
|
-
detail_values['Description'.to_sym] = main_content.css('p').text.strip
|
|
63
|
+
detail_values['Description'.to_sym] = main_content.css('p').text.strip # Description must be first attribute.
|
|
64
64
|
detail_values['Condition'.to_sym] = item_condition
|
|
65
65
|
|
|
66
66
|
# Create the rest from scraping the html's detail attrribute/value table.
|
|
@@ -68,7 +68,7 @@ private
|
|
|
68
68
|
(0...detail_cells.size).each { |index|
|
|
69
69
|
dl_tag = detail_cells[index].children
|
|
70
70
|
(0...dl_tag.size).step(2) { |child|
|
|
71
|
-
attribute = dl_tag[child].text
|
|
71
|
+
attribute = dl_tag[child].text.chomp(':')
|
|
72
72
|
value = dl_tag[child+1].text
|
|
73
73
|
detail_values[attribute.to_sym] = value
|
|
74
74
|
}
|
|
@@ -94,7 +94,7 @@ private
|
|
|
94
94
|
if 0 < dl_tag.size # need to do alternate normal processing.
|
|
95
95
|
process_detail_list_alt(dl_tag, detail_values)
|
|
96
96
|
else
|
|
97
|
-
attribute = attribute_tag.text
|
|
97
|
+
attribute = attribute_tag.text.chomp(':')
|
|
98
98
|
value_tag = detail_cells[index].children[3]
|
|
99
99
|
|
|
100
100
|
if value_tag
|
|
@@ -121,7 +121,7 @@ private
|
|
|
121
121
|
(0...doc.size).step(4) { |index|
|
|
122
122
|
attribute = doc[index+1]
|
|
123
123
|
next if attribute.nil?
|
|
124
|
-
attr_text = attribute.text
|
|
124
|
+
attr_text = attribute.text.chomp(':')
|
|
125
125
|
value_tag = doc[index+3]
|
|
126
126
|
detail_values[attr_text.to_sym] = value_tag.text if value_tag
|
|
127
127
|
}
|
|
@@ -133,7 +133,7 @@ private
|
|
|
133
133
|
children = doc[index].children
|
|
134
134
|
child_index = 1
|
|
135
135
|
while child_index < children.size
|
|
136
|
-
attribute = children[child_index].text
|
|
136
|
+
attribute = children[child_index].text.chomp(':')
|
|
137
137
|
value = children[child_index+2]
|
|
138
138
|
if value.nil? || value.text.strip.size == 0
|
|
139
139
|
value = children[child_index+1]
|
data/lib/classifieds/item.rb
CHANGED
|
@@ -12,7 +12,7 @@ class Classifieds::Item # describes the thing in a listing that is for sale
|
|
|
12
12
|
|
|
13
13
|
# Empty list of created objects
|
|
14
14
|
def self.clear
|
|
15
|
-
# all.clear
|
|
15
|
+
# all.clear # not used, for now.
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
COLUMN_SEPARATION = 5
|
|
@@ -21,49 +21,74 @@ class Classifieds::Item # describes the thing in a listing that is for sale
|
|
|
21
21
|
def details_to_string(addon_details)
|
|
22
22
|
Classifieds::Listing.scrape_listing_details(self.class, @detail_url, @condition, @detail_values) if @detail_values.empty?
|
|
23
23
|
|
|
24
|
+
# Setup attribute/value details array
|
|
24
25
|
detail_values_array = @detail_values.to_a
|
|
25
|
-
addon_details.delete(:Phone) if detail_phone # do not
|
|
26
|
+
addon_details.delete(:Phone) if detail_phone? # do not addon phone if item details has a phone.
|
|
26
27
|
detail_values_array.concat(addon_details.to_a)
|
|
27
|
-
|
|
28
|
-
mod2 = detail_values_array.size % 2 # and account for an odd number of details.
|
|
29
|
-
col1_ljust = max_col1_width(detail_values_array, offset+mod2) + COLUMN_SEPARATION
|
|
30
|
-
result = ''
|
|
28
|
+
col2_offset = ((detail_values_array.size-1) / 2) + ((detail_values_array.size-1) % 2) # remove 1 from array size for description. It will get its own row.
|
|
31
29
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
30
|
+
col_attr_width = []
|
|
31
|
+
col_width = []
|
|
32
|
+
|
|
33
|
+
# Calculate column 1 widths
|
|
34
|
+
widths = max_col_widths(detail_values_array, 0, col2_offset) # [max_attr_width, max_val_width, max_column_width]
|
|
35
|
+
col_attr_width[0] = widths[0]
|
|
36
|
+
col_width[0] = [widths[2], Classifieds::Item.col_width_limit(1)].min # limit col width to max col width
|
|
37
|
+
col_width[0] += COLUMN_SEPARATION
|
|
38
|
+
|
|
39
|
+
# Calculate column 2 widths
|
|
40
|
+
widths = max_col_widths(detail_values_array, col2_offset+1, detail_values_array.size) # [max_attr_width, max_val_width, max_column_width]
|
|
41
|
+
col_attr_width[1] = widths[0]
|
|
42
|
+
col_width[1] = [widths[2], Classifieds::Item.col_width_limit(2)].min # limit col width to max col width
|
|
37
43
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
# Description value spans all columns.
|
|
45
|
+
attribute = detail_values_array[0][0].to_s
|
|
46
|
+
value = detail_values_array[0][1]
|
|
47
|
+
result = " #{Classifieds::Listing.format_detail(attribute, col_attr_width[0], value)}\n"
|
|
48
|
+
|
|
49
|
+
# Then display remaining details in two column format.
|
|
50
|
+
(1..col2_offset).each { |index|
|
|
51
|
+
result << " #{Classifieds::Item.format_pair(detail_values_array, index, col_attr_width[0], col_width[0])}"
|
|
52
|
+
next if (index+col2_offset) == detail_values_array.size # when odd number of details.
|
|
53
|
+
result << " #{Classifieds::Item.format_pair(detail_values_array, index+col2_offset, col_attr_width[1], col_width[1])}\n"
|
|
46
54
|
}
|
|
47
55
|
result
|
|
48
56
|
end
|
|
49
57
|
|
|
50
|
-
def detail_phone
|
|
51
|
-
@detail_values[:Phone]
|
|
52
|
-
end
|
|
53
|
-
|
|
54
58
|
## PRIVATE METHODS
|
|
55
59
|
private
|
|
56
60
|
|
|
57
|
-
#
|
|
58
|
-
def
|
|
59
|
-
|
|
60
|
-
|
|
61
|
+
# Return attribute field width for given column
|
|
62
|
+
def self.col_width_limit(col)
|
|
63
|
+
[40, 40][col-1]
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Do the details contain a phone entry?
|
|
67
|
+
def detail_phone?
|
|
68
|
+
@detail_values[:Phone] ? true : false
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Formats an attribute/value pair for printing
|
|
72
|
+
def self.format_pair(array, index, attr_width, col_width)
|
|
73
|
+
attribute = array[index][0].to_s
|
|
74
|
+
value = array[index][1]
|
|
75
|
+
Classifieds::Listing.format_detail(attribute, attr_width, value).slice(0,col_width).ljust(col_width)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Find widths of widest column data
|
|
79
|
+
# Returns [max_attr_width, max_val_width, max_column_width]
|
|
80
|
+
def max_col_widths(detail_values_array, start_index, end_index)
|
|
81
|
+
max_attr_width = 0
|
|
82
|
+
max_val_width = 0
|
|
83
|
+
(start_index...end_index).each { |index|
|
|
61
84
|
attribute = detail_values_array[index][0].to_s
|
|
62
|
-
|
|
85
|
+
width = Classifieds::Listing.format_detail_attr(attribute, 0).size
|
|
86
|
+
max_attr_width = width if width > max_attr_width
|
|
87
|
+
next if 'Description' == attribute # Description spans all cols, so don't consider its value width.
|
|
63
88
|
value = detail_values_array[index][1]
|
|
64
|
-
|
|
65
|
-
|
|
89
|
+
width = Classifieds::Listing.format_detail_val(value, 0).size
|
|
90
|
+
max_val_width = width if width > max_val_width
|
|
66
91
|
}
|
|
67
|
-
|
|
92
|
+
[max_attr_width, max_val_width, max_attr_width + max_val_width + ': '.size]
|
|
68
93
|
end
|
|
69
94
|
end
|
data/lib/classifieds/listing.rb
CHANGED
|
@@ -28,7 +28,7 @@ class Classifieds::Listing # describes a classified advertisement
|
|
|
28
28
|
|
|
29
29
|
# Prints the specified summary listings for the specified item subclass
|
|
30
30
|
def self.print_summary(item_class, start_index, end_index)
|
|
31
|
-
puts " #{item_class.summary_header}
|
|
31
|
+
puts " #{item_class.summary_header} #{Classifieds::Seller.summary_header} #{lfmt('List Date', 10)}"
|
|
32
32
|
all[start_index..end_index].each_with_index { |listing, index| puts listing.summary_detail_row(start_index+index+1) }
|
|
33
33
|
end
|
|
34
34
|
|
|
@@ -47,30 +47,30 @@ class Classifieds::Listing # describes a classified advertisement
|
|
|
47
47
|
|
|
48
48
|
# Prints a summary detail row
|
|
49
49
|
def summary_detail_row(item_number)
|
|
50
|
-
"#{(item_number).to_s.rjust(2)}. #{@item.summary_detail}
|
|
50
|
+
"#{(item_number).to_s.rjust(2)}. #{@item.summary_detail} #{@seller.summary_detail} #{Classifieds::Listing.lfmt(@start_date, 10)}"
|
|
51
51
|
end
|
|
52
52
|
|
|
53
53
|
# Formats an array of strings according to an array of formats
|
|
54
|
-
def self.
|
|
54
|
+
def self.format_cols(values, formats)
|
|
55
55
|
result = ''
|
|
56
|
-
values.each_with_index { |value, index| result << "#{
|
|
56
|
+
values.each_with_index { |value, index| result << "#{format_col(value, formats[index][0], formats[index][1])} " }
|
|
57
57
|
result
|
|
58
58
|
end
|
|
59
59
|
|
|
60
60
|
MAX_LINE_LENGTH = 100
|
|
61
61
|
|
|
62
62
|
# Format a detail attribute
|
|
63
|
-
def self.
|
|
63
|
+
def self.format_detail_attr(string, width)
|
|
64
64
|
"#{lfmt(string, width)}"
|
|
65
65
|
end
|
|
66
66
|
|
|
67
67
|
# Format a detail item
|
|
68
68
|
def self.format_detail(attr, attr_width, val)
|
|
69
|
-
"#{
|
|
69
|
+
"#{format_detail_attr(attr, attr_width)}: #{format_detail_val(val, attr_width)}"
|
|
70
70
|
end
|
|
71
71
|
|
|
72
72
|
# Format a detail value (with wrap if necessary)
|
|
73
|
-
def self.
|
|
73
|
+
def self.format_detail_val(string, wrap_indent)
|
|
74
74
|
new_string = string.strip
|
|
75
75
|
if new_string.size > MAX_LINE_LENGTH
|
|
76
76
|
new_string = ''
|
|
@@ -101,7 +101,7 @@ class Classifieds::Listing # describes a classified advertisement
|
|
|
101
101
|
end
|
|
102
102
|
|
|
103
103
|
# Format a column
|
|
104
|
-
def self.
|
|
104
|
+
def self.format_col(str, width, justify)
|
|
105
105
|
case justify
|
|
106
106
|
when 'l'
|
|
107
107
|
"#{lfmt(str, width)}"
|
|
@@ -114,7 +114,7 @@ class Classifieds::Listing # describes a classified advertisement
|
|
|
114
114
|
|
|
115
115
|
# Left justify string and pad or trim to size
|
|
116
116
|
def self.lfmt(string, size)
|
|
117
|
-
string.slice(0,size).ljust(size)
|
|
117
|
+
size == 0 ? string : string.slice(0,size).ljust(size)
|
|
118
118
|
end
|
|
119
119
|
|
|
120
120
|
# Right justify string and pad or trim to size
|
data/lib/classifieds/seller.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
class Classifieds::Seller # describes the entity that is selling the .Item in a .Listing
|
|
2
2
|
# A seller is uniquely identified by name + location + phone
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
SUMMARY_COL_FORMATS = [[28,'l'], [32,'l']] # [width, justification]
|
|
5
5
|
|
|
6
6
|
@@all_sellers = []
|
|
7
7
|
|
|
@@ -34,12 +34,12 @@ class Classifieds::Seller # describes the entity that is selling the .Item in a
|
|
|
34
34
|
|
|
35
35
|
# Return a summary listing detail row
|
|
36
36
|
def summary_detail
|
|
37
|
-
Classifieds::Listing.
|
|
37
|
+
Classifieds::Listing.format_cols([@name, @location], SUMMARY_COL_FORMATS)
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
# Return the summary listing summary title row
|
|
41
41
|
def self.summary_header
|
|
42
|
-
Classifieds::Listing.
|
|
42
|
+
Classifieds::Listing.format_cols(['Seller', 'Location'], SUMMARY_COL_FORMATS)
|
|
43
43
|
end
|
|
44
44
|
|
|
45
45
|
## PRIVATE METHODS
|
data/lib/classifieds/version.rb
CHANGED