efo_nelfo 1.2.0 → 1.3.0
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/README.md +6 -6
- data/lib/efo_nelfo/errors.rb +1 -0
- data/lib/efo_nelfo/post_head_type.rb +4 -8
- data/lib/efo_nelfo/post_type.rb +9 -1
- data/lib/efo_nelfo/properties.rb +121 -0
- data/lib/efo_nelfo/property.rb +70 -131
- data/lib/efo_nelfo/property_types.rb +47 -0
- data/lib/efo_nelfo/reader/csv.rb +3 -1
- data/lib/efo_nelfo/v40/vl.rb +17 -0
- data/lib/efo_nelfo/v40/vx.rb +18 -5
- data/lib/efo_nelfo/version.rb +1 -1
- data/lib/efo_nelfo.rb +2 -0
- data/spec/csv_writer_spec.rb +1 -1
- data/spec/post_type_spec.rb +20 -12
- data/spec/properties_spec.rb +149 -0
- data/spec/property_spec.rb +148 -99
- data/spec/vl_spec.rb +36 -0
- metadata +7 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 64cdaf33e1a391067ac19395f549997d497faa45
|
4
|
+
data.tar.gz: 247909b6de770da10400d1ba2ff6ac8ad16b2a2e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d3dbe7d16a9b78ee13b12a6ee47657481376a9bc73017f1d83caa11e22759f6f3e838d9b0878f7615ecd10b9cfe58af7355716863d215a53cdfbe247b82890a4
|
7
|
+
data.tar.gz: 035aaa3183fd806377ccc9ca7bcabb6a6e5fbd0f174d5625ef831ae89fb0e9054b78a5af7c55c58282bf5ff3f41c0d8bdc003bd3432a58d958eb2988f5ad3e2a
|
data/README.md
CHANGED
@@ -2,14 +2,15 @@
|
|
2
2
|
|
3
3
|
Gem for parsing and writing EfoNelfo documents.
|
4
4
|
|
5
|
-
Supported versions:
|
5
|
+
Supported EfoNelfo versions:
|
6
6
|
|
7
7
|
* 4.0
|
8
8
|
|
9
9
|
Supported formats:
|
10
10
|
|
11
|
-
* Bestilling
|
12
|
-
|
11
|
+
* Bestilling (BH, BL, BT)
|
12
|
+
* Vareformat (VH, VL, VX, VA)
|
13
|
+
* Rabatt (RH, RL)
|
13
14
|
|
14
15
|
## Usage
|
15
16
|
|
@@ -19,8 +20,8 @@ Importing a CSV file:
|
|
19
20
|
|
20
21
|
Exporting CSV:
|
21
22
|
|
22
|
-
# order = EfoNelfo::V40::
|
23
|
-
# order.add EfoNelfo::V40::
|
23
|
+
# order = EfoNelfo::V40::VH.new
|
24
|
+
# order.add EfoNelfo::V40::VL.new item_number: '442', order_number: 'abc'
|
24
25
|
# order.to_csv
|
25
26
|
|
26
27
|
|
@@ -31,7 +32,6 @@ Exporting CSV:
|
|
31
32
|
* Support more versions
|
32
33
|
* Support XML
|
33
34
|
|
34
|
-
|
35
35
|
## Resources
|
36
36
|
|
37
37
|
* http://www.efo.no/Portals/5/docs/ImplementasjonsGuide%20EFONELFO%204.0.pdf
|
data/lib/efo_nelfo/errors.rb
CHANGED
@@ -8,6 +8,10 @@ module EfoNelfo
|
|
8
8
|
base.send :property, :version, alias: :Versjon, limit: 3, default: base.version
|
9
9
|
end
|
10
10
|
|
11
|
+
def [](index)
|
12
|
+
lines[index]
|
13
|
+
end
|
14
|
+
|
11
15
|
def add(post_type)
|
12
16
|
if has_association? post_type
|
13
17
|
find_association(post_type) << post_type
|
@@ -18,13 +22,5 @@ module EfoNelfo
|
|
18
22
|
end
|
19
23
|
end
|
20
24
|
|
21
|
-
def to_csv
|
22
|
-
CSV.generate EfoNelfo::Reader::CSV::CSV_OPTIONS do |csv|
|
23
|
-
to_a.each do |row|
|
24
|
-
csv << row unless row.empty?
|
25
|
-
end
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
29
25
|
end
|
30
26
|
end
|
data/lib/efo_nelfo/post_type.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
module EfoNelfo
|
2
2
|
|
3
3
|
class PostType
|
4
|
-
include EfoNelfo::
|
4
|
+
include EfoNelfo::Properties
|
5
5
|
include EfoNelfo::HasMany
|
6
6
|
|
7
7
|
class << self
|
@@ -49,6 +49,14 @@ module EfoNelfo
|
|
49
49
|
self.class.version
|
50
50
|
end
|
51
51
|
|
52
|
+
def to_csv
|
53
|
+
CSV.generate EfoNelfo::Reader::CSV::CSV_OPTIONS do |csv|
|
54
|
+
to_a.each do |row|
|
55
|
+
csv << row unless row.empty?
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
52
60
|
end
|
53
61
|
|
54
62
|
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
module EfoNelfo
|
2
|
+
|
3
|
+
module Properties
|
4
|
+
|
5
|
+
def self.included(base)
|
6
|
+
base.send :extend, ClassMethods
|
7
|
+
end
|
8
|
+
|
9
|
+
def initialize_attributes(*args)
|
10
|
+
if args && args.first.is_a?(Hash)
|
11
|
+
args.first.each do |attr, value|
|
12
|
+
send "#{attr}=", value
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# Returns all properties
|
18
|
+
def properties
|
19
|
+
@properties ||= initialize_properties
|
20
|
+
end
|
21
|
+
alias :attributes :properties
|
22
|
+
|
23
|
+
# Returns true if the given property exists
|
24
|
+
def has_property?(name)
|
25
|
+
properties.include? name
|
26
|
+
end
|
27
|
+
|
28
|
+
# Returns all property values as array formatted for csv
|
29
|
+
def to_a
|
30
|
+
properties.values.map &:to_csv
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def initialize_properties
|
36
|
+
self.class.properties.inject({}) do |hash,(name,options)|
|
37
|
+
hash[name] = EfoNelfo::Property.new(name, options)
|
38
|
+
hash
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
module ClassMethods
|
43
|
+
# Creates an attribute with given name.
|
44
|
+
#
|
45
|
+
# Options
|
46
|
+
# - type String, Integer etc. Default is String
|
47
|
+
# - required whether attribute is required. Default is false
|
48
|
+
# - limit Length the attribute can be. Default is nil
|
49
|
+
# - alias Norwegian alias name for the attribute
|
50
|
+
#
|
51
|
+
def property(name, options={})
|
52
|
+
options = {
|
53
|
+
type: :string,
|
54
|
+
required: false,
|
55
|
+
}.update options
|
56
|
+
|
57
|
+
name = name.to_sym
|
58
|
+
alias_name = options.delete(:alias)
|
59
|
+
|
60
|
+
# ensure all options are valid
|
61
|
+
EfoNelfo::Property.validate_options! options
|
62
|
+
|
63
|
+
# ensure property is unique
|
64
|
+
raise EfoNelfo::DuplicateProperty if properties.has_key?(name)
|
65
|
+
|
66
|
+
# setup getters and setters
|
67
|
+
create_reader_for(name, options)
|
68
|
+
create_setter_for(name, options) unless options[:read_only]
|
69
|
+
create_question_for(name) if options[:type] == :boolean
|
70
|
+
create_alias_for(name, alias_name, options) if alias_name
|
71
|
+
|
72
|
+
properties[name] = options
|
73
|
+
end
|
74
|
+
|
75
|
+
# Returns all properties defined for the class
|
76
|
+
def properties
|
77
|
+
@_properties ||= {}
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
# Creates an attribute accessor for the property
|
83
|
+
def create_reader_for(name, options)
|
84
|
+
define_method name do
|
85
|
+
attributes[name].value
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# Creates an attribute setter for the property
|
90
|
+
def create_setter_for(name, options)
|
91
|
+
define_method "#{name}=" do |value|
|
92
|
+
attributes[name].value = value
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# Creates a name? accessor for the property
|
97
|
+
# Only applies to boolean types
|
98
|
+
def create_question_for(name)
|
99
|
+
define_method "#{name}?" do
|
100
|
+
attributes[name].value == true
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# Create an alias getter/setter for the property
|
105
|
+
def create_alias_for(name, alias_name, options)
|
106
|
+
define_method(alias_name) do
|
107
|
+
send name
|
108
|
+
end
|
109
|
+
|
110
|
+
unless options[:read_only]
|
111
|
+
define_method("#{alias_name}=") do |val|
|
112
|
+
send "#{name}=", val
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
end
|
118
|
+
|
119
|
+
end
|
120
|
+
|
121
|
+
end
|
data/lib/efo_nelfo/property.rb
CHANGED
@@ -1,155 +1,94 @@
|
|
1
|
-
|
1
|
+
using EfoNelfo::PropertyTypes
|
2
2
|
|
3
|
-
|
3
|
+
module EfoNelfo
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
|
5
|
+
class Property
|
6
|
+
VALID_OPTIONS = [:type, :required, :limit, :read_only, :default, :decimals]
|
7
|
+
VALID_TYPES = [:string, :integer, :boolean, :date]
|
8
8
|
|
9
|
-
|
10
|
-
if args && args.first.is_a?(Hash)
|
11
|
-
args.first.each do |attr, value|
|
12
|
-
send "#{attr}=", value
|
13
|
-
end
|
14
|
-
end
|
15
|
-
end
|
16
|
-
|
17
|
-
def attributes
|
18
|
-
@attributes ||= initialize_default_attributes
|
19
|
-
end
|
9
|
+
attr_reader :name, :options, :value
|
20
10
|
|
21
|
-
def
|
22
|
-
|
11
|
+
def self.validate_options!(options)
|
12
|
+
invalid_options = options.keys - VALID_OPTIONS
|
13
|
+
raise EfoNelfo::UnknownPropertyOption.new("Invalid options: #{invalid_options.join(',')}") if invalid_options.any?
|
14
|
+
raise EfoNelfo::InvalidPropertyType.new("Valid types are #{VALID_TYPES.join(',')}") unless VALID_TYPES.include?(options[:type])
|
23
15
|
end
|
24
16
|
|
25
|
-
def
|
26
|
-
properties.include? property
|
27
|
-
end
|
17
|
+
def initialize(name, defaults={})
|
28
18
|
|
29
|
-
|
30
|
-
|
31
|
-
|
19
|
+
options = {
|
20
|
+
type: :string,
|
21
|
+
required: false,
|
22
|
+
default: nil,
|
23
|
+
read_only: false,
|
24
|
+
decimals: nil,
|
25
|
+
limit: 100
|
26
|
+
}
|
27
|
+
options.update(defaults) if defaults.is_a?(Hash)
|
32
28
|
|
33
|
-
|
29
|
+
self.class.validate_options! options
|
34
30
|
|
35
|
-
|
36
|
-
|
31
|
+
@name = name
|
32
|
+
@options = options
|
33
|
+
@value = options[:default]
|
37
34
|
end
|
38
35
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
36
|
+
# Assign a value to the property
|
37
|
+
# The value is converted to whatever specified by options[:type]
|
38
|
+
# Examples:
|
39
|
+
# (boolean) value = 'J' # => true
|
40
|
+
# (boolean) value = false # => false
|
41
|
+
# (date) value = '20120101' # => Date.new(2012,1,1)
|
42
|
+
# (integer) value = '2' # => 2
|
43
|
+
# (string) value = 'foo' # => 'foo'
|
44
|
+
def value=(new_value)
|
45
|
+
return nil if readonly?
|
46
|
+
|
47
|
+
@value = case
|
48
|
+
when boolean?
|
49
|
+
new_value.nil? || new_value == true || new_value == 'J' || new_value == 'j' || new_value == '' ? true : false
|
50
|
+
when date?
|
51
|
+
new_value.is_a?(Date) ? new_value : Date.parse(new_value) rescue nil
|
52
|
+
when integer?
|
53
|
+
new_value.nil? ? nil : new_value.to_i
|
48
54
|
else
|
49
|
-
|
50
|
-
end
|
51
|
-
when :boolean
|
52
|
-
value.nil? || value == true || value == 'J' || value == '' ? true : false
|
53
|
-
else
|
54
|
-
value
|
55
|
+
new_value
|
55
56
|
end
|
56
57
|
end
|
57
58
|
|
58
|
-
#
|
59
|
-
def
|
60
|
-
|
61
|
-
if respond_to?("format_#{attr}")
|
62
|
-
value = send "format_#{attr}"
|
63
|
-
else
|
64
|
-
value = send attr
|
65
|
-
|
66
|
-
type = properties[attr][:type]
|
67
|
-
|
68
|
-
value = case type
|
69
|
-
when :date
|
70
|
-
value ? value.strftime("%Y%m%d") : nil
|
71
|
-
when :boolean
|
72
|
-
value == true ? "J" : nil
|
73
|
-
else
|
74
|
-
value
|
75
|
-
end
|
76
|
-
end
|
77
|
-
value.respond_to?(:encode) ? value.encode(Encoding::ISO_8859_1) : value
|
59
|
+
# returns formatted value suitable for csv output
|
60
|
+
def to_csv
|
61
|
+
output = value.to_csv
|
78
62
|
end
|
79
63
|
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
def property(name, options={})
|
90
|
-
|
91
|
-
options = {
|
92
|
-
type: :string,
|
93
|
-
required: false,
|
94
|
-
}.update options
|
95
|
-
|
96
|
-
name = name.to_sym
|
97
|
-
|
98
|
-
# ensure all options are valid
|
99
|
-
valid_options = [:type, :required, :limit, :read_only, :alias, :default]
|
100
|
-
invalid_options = options.keys - valid_options
|
101
|
-
raise EfoNelfo::UnknownPropertyOption.new("Invalid option for #{name}: #{invalid_options.join(',')}") if invalid_options.any?
|
102
|
-
|
103
|
-
# Store property info in @properties
|
104
|
-
raise EfoNelfo::DuplicateProperty if properties.has_key?(name)
|
105
|
-
properties[name] = options
|
106
|
-
|
107
|
-
create_reader_for(name, options)
|
108
|
-
create_setter_for(name, options) unless options[:read_only]
|
109
|
-
create_question_for(name) if options[:type] == :boolean
|
110
|
-
create_alias_for(name, options) if options[:alias]
|
111
|
-
end
|
112
|
-
|
113
|
-
# Returns all properties
|
114
|
-
def properties
|
115
|
-
@_properties ||= {}
|
116
|
-
end
|
117
|
-
|
118
|
-
private
|
119
|
-
|
120
|
-
# Creates an attribute accessor for name
|
121
|
-
def create_reader_for(name, options)
|
122
|
-
define_method name do
|
123
|
-
attributes[name]
|
124
|
-
end
|
125
|
-
end
|
126
|
-
|
127
|
-
# Creates an attribute setter for name
|
128
|
-
def create_setter_for(name, options)
|
129
|
-
define_method "#{name}=" do |value|
|
130
|
-
attributes[name] = format_value(value, options[:type])
|
131
|
-
end
|
132
|
-
end
|
64
|
+
# Returns integer to floating point based on specified decimals
|
65
|
+
# Example:
|
66
|
+
# If decimal is set to 4, and the value is 4000
|
67
|
+
# then to_f returns 0.4
|
68
|
+
def to_f
|
69
|
+
return nil if value.nil?
|
70
|
+
value.to_f.with_decimals decimals
|
71
|
+
end
|
72
|
+
alias :to_decimal :to_f
|
133
73
|
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
end
|
139
|
-
end
|
74
|
+
# Returns true if the property is read only
|
75
|
+
def readonly?
|
76
|
+
options[:read_only]
|
77
|
+
end
|
140
78
|
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
79
|
+
# Returns true if the property is required
|
80
|
+
# Note: this is not in use yet
|
81
|
+
def required?
|
82
|
+
options[:required]
|
83
|
+
end
|
145
84
|
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
end
|
151
|
-
end
|
85
|
+
def boolean?; type == :boolean; end
|
86
|
+
def date?; type == :date; end
|
87
|
+
def integer?; type == :integer; end
|
88
|
+
def string?; type == :string; end
|
152
89
|
|
90
|
+
def method_missing(*args)
|
91
|
+
options.has_key?(args.first) ? options[args.first] : super
|
153
92
|
end
|
154
93
|
|
155
94
|
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module EfoNelfo
|
2
|
+
module PropertyTypes
|
3
|
+
refine NilClass do
|
4
|
+
def to_csv
|
5
|
+
nil
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
refine TrueClass do
|
10
|
+
def to_csv
|
11
|
+
'J'
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
refine FalseClass do
|
16
|
+
def to_csv
|
17
|
+
'N'
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
refine String do
|
22
|
+
def to_csv
|
23
|
+
to_s.encode EfoNelfo::Reader::CSV::ENCODING
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
refine Date do
|
28
|
+
def to_csv
|
29
|
+
strftime("%Y%m%d")
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
refine Fixnum do
|
34
|
+
def to_csv
|
35
|
+
to_i
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
refine Float do
|
40
|
+
def with_decimals(decimals)
|
41
|
+
decimals.nil? ? self : self * (1.0/10**decimals.to_i)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
data/lib/efo_nelfo/reader/csv.rb
CHANGED
@@ -4,6 +4,8 @@ module EfoNelfo
|
|
4
4
|
module Reader
|
5
5
|
|
6
6
|
class CSV
|
7
|
+
ENCODING = Encoding::ISO_8859_1
|
8
|
+
|
7
9
|
CSV_OPTIONS = {
|
8
10
|
col_sep: ';',
|
9
11
|
headers: false,
|
@@ -17,7 +19,7 @@ module EfoNelfo
|
|
17
19
|
|
18
20
|
def initialize(options)
|
19
21
|
if options[:filename]
|
20
|
-
@data = File.read(options[:filename], encoding:
|
22
|
+
@data = File.read(options[:filename], encoding: ENCODING)
|
21
23
|
else
|
22
24
|
@data = options[:data]
|
23
25
|
end
|
data/lib/efo_nelfo/v40/vl.rb
CHANGED
@@ -33,6 +33,23 @@ module EfoNelfo
|
|
33
33
|
info.map(&:image).compact
|
34
34
|
end
|
35
35
|
|
36
|
+
[ :weight, :dimension, :volume, :fdv, :hms ].each do |key|
|
37
|
+
define_method key do
|
38
|
+
pick key
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Returns array of urls extracted from VX lines
|
43
|
+
def urls
|
44
|
+
info.map(&:value).select { |u| u.match %r{\Ahttps?://} }
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def pick(method)
|
50
|
+
vx = info.select(&"#{method}?".to_sym).compact.first
|
51
|
+
vx && vx.send(method)
|
52
|
+
end
|
36
53
|
end
|
37
54
|
end
|
38
55
|
end
|
data/lib/efo_nelfo/v40/vx.rb
CHANGED
@@ -5,12 +5,25 @@ module EfoNelfo
|
|
5
5
|
property :field, alias: :FeltId, limit: 10, required: true
|
6
6
|
property :value, alias: :Verdi, limit: 70, required: true
|
7
7
|
|
8
|
-
|
9
|
-
|
10
|
-
|
8
|
+
FIELD_MAPPINGS = {
|
9
|
+
image: "BILDE",
|
10
|
+
weight: "VEKT",
|
11
|
+
dimension: "DIMENSJON",
|
12
|
+
fdv: "FDV",
|
13
|
+
hms: "HMS",
|
14
|
+
volume: "VOLUM"
|
15
|
+
}
|
16
|
+
|
17
|
+
FIELD_MAPPINGS.each do |key, field_value|
|
18
|
+
# Add methods like def image?; field == 'BILDE'; end
|
19
|
+
define_method "#{key}?" do
|
20
|
+
field == field_value
|
21
|
+
end
|
11
22
|
|
12
|
-
|
13
|
-
|
23
|
+
# Add methods like: def image; value if image?; end
|
24
|
+
define_method "#{key}" do
|
25
|
+
self.value if send("#{key}?")
|
26
|
+
end
|
14
27
|
end
|
15
28
|
|
16
29
|
end
|
data/lib/efo_nelfo/version.rb
CHANGED
data/lib/efo_nelfo.rb
CHANGED
@@ -3,7 +3,9 @@
|
|
3
3
|
# Common stuff
|
4
4
|
require 'efo_nelfo/version'
|
5
5
|
require 'efo_nelfo/errors'
|
6
|
+
require 'efo_nelfo/property_types'
|
6
7
|
require 'efo_nelfo/property'
|
8
|
+
require 'efo_nelfo/properties'
|
7
9
|
require 'efo_nelfo/collection'
|
8
10
|
require 'efo_nelfo/has_many'
|
9
11
|
require 'efo_nelfo/post_type'
|
data/spec/csv_writer_spec.rb
CHANGED
data/spec/post_type_spec.rb
CHANGED
@@ -4,15 +4,15 @@ describe EfoNelfo::PostType do
|
|
4
4
|
|
5
5
|
module EfoNelfo
|
6
6
|
module V21
|
7
|
-
class
|
8
|
-
property :post_type
|
9
|
-
property :version
|
7
|
+
class AB < EfoNelfo::PostType
|
8
|
+
property :post_type, default: 'AB'
|
9
|
+
property :version, default: '21'
|
10
10
|
property :seller_id
|
11
|
-
has_many :lines, :post_type => "
|
11
|
+
has_many :lines, :post_type => "XY"
|
12
12
|
end
|
13
13
|
|
14
|
-
class
|
15
|
-
property :post_type
|
14
|
+
class XY < EfoNelfo::PostType
|
15
|
+
property :post_type, default: 'XY'
|
16
16
|
property :order_number
|
17
17
|
end
|
18
18
|
end
|
@@ -21,27 +21,35 @@ describe EfoNelfo::PostType do
|
|
21
21
|
describe "versioning" do
|
22
22
|
|
23
23
|
it ".version_from_class returns the version" do
|
24
|
-
EfoNelfo::V21::
|
24
|
+
EfoNelfo::V21::AB.version_from_class.must_equal "21"
|
25
25
|
end
|
26
26
|
|
27
27
|
it ".version returns the version as a number" do
|
28
|
-
EfoNelfo::V21::
|
28
|
+
EfoNelfo::V21::AB.version.must_equal "2.1"
|
29
29
|
end
|
30
30
|
|
31
31
|
it ".version_to_namespace converts version to module name" do
|
32
|
-
EfoNelfo::V21::
|
32
|
+
EfoNelfo::V21::AB.version_to_namespace("2.1").must_equal "21"
|
33
33
|
end
|
34
34
|
|
35
35
|
end
|
36
36
|
|
37
|
+
describe "to_csv" do
|
38
|
+
it "converts the posttype to csv" do
|
39
|
+
pt = EfoNelfo::V21::AB.new seller_id: '123'
|
40
|
+
pt.lines << EfoNelfo::V21::XY.new(order_number: '41')
|
41
|
+
pt.to_csv.must_match /AB;21;123.+XY;41/m
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
37
45
|
describe ".from" do
|
38
46
|
let(:hash) {
|
39
47
|
{
|
40
|
-
post_type: "
|
48
|
+
post_type: "AB",
|
41
49
|
version: "2.1",
|
42
50
|
seller_id: "123",
|
43
51
|
lines: [
|
44
|
-
{ post_type: "
|
52
|
+
{ post_type: "XY", order_number: "666-2" }
|
45
53
|
]
|
46
54
|
}
|
47
55
|
}
|
@@ -49,7 +57,7 @@ describe EfoNelfo::PostType do
|
|
49
57
|
let(:pt) { EfoNelfo::PostType.from hash }
|
50
58
|
|
51
59
|
it "converts the hash into a valid posttype" do
|
52
|
-
pt.must_be_instance_of EfoNelfo::V21::
|
60
|
+
pt.must_be_instance_of EfoNelfo::V21::AB
|
53
61
|
pt.seller_id.must_equal "123"
|
54
62
|
end
|
55
63
|
|
@@ -0,0 +1,149 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'spec_helper'
|
3
|
+
|
4
|
+
describe EfoNelfo::Properties do
|
5
|
+
|
6
|
+
class Foo
|
7
|
+
include EfoNelfo::Properties
|
8
|
+
property :foo, alias: "Føbar", limit: 3, default: 'I am foo'
|
9
|
+
property :bar
|
10
|
+
property :date, type: :date
|
11
|
+
property :number, type: :integer
|
12
|
+
property :doable, type: :boolean, default: false
|
13
|
+
property :immutable, read_only: true, default: 'NO'
|
14
|
+
end
|
15
|
+
|
16
|
+
let(:obj) { Foo.new }
|
17
|
+
|
18
|
+
it "raises an error if trying to add duplicate property" do
|
19
|
+
lambda {
|
20
|
+
class Bar
|
21
|
+
include EfoNelfo::Properties
|
22
|
+
property :foo
|
23
|
+
property :foo
|
24
|
+
end
|
25
|
+
}.must_raise EfoNelfo::DuplicateProperty
|
26
|
+
end
|
27
|
+
|
28
|
+
it "raises an error if trying to assign an unsupported option" do
|
29
|
+
lambda {
|
30
|
+
class Bar
|
31
|
+
include EfoNelfo::Properties
|
32
|
+
property :lame, funny: true
|
33
|
+
end
|
34
|
+
}.must_raise EfoNelfo::UnknownPropertyOption
|
35
|
+
end
|
36
|
+
|
37
|
+
it "adds a getter method for :foo" do
|
38
|
+
obj.foo.must_equal 'I am foo'
|
39
|
+
end
|
40
|
+
|
41
|
+
it "adds a setter method for :foo" do
|
42
|
+
obj.foo = 'Test'
|
43
|
+
obj.foo.must_equal 'Test'
|
44
|
+
end
|
45
|
+
|
46
|
+
it "adds a question method for boolean types" do
|
47
|
+
obj.doable?.must_equal false
|
48
|
+
obj.doable = true
|
49
|
+
obj.doable?.must_equal true
|
50
|
+
end
|
51
|
+
|
52
|
+
it "won't do any encoding conversions" do
|
53
|
+
obj.foo = 'Sjøhest'
|
54
|
+
obj.foo.encoding.name.must_equal 'UTF-8'
|
55
|
+
end
|
56
|
+
|
57
|
+
it "adds an alias getter and setter for foo" do
|
58
|
+
obj.foo = 'Test'
|
59
|
+
obj.Føbar.must_equal 'Test'
|
60
|
+
obj.Føbar = 'Hacked'
|
61
|
+
obj.Føbar.must_equal 'Hacked'
|
62
|
+
obj.foo.must_equal 'Hacked'
|
63
|
+
end
|
64
|
+
|
65
|
+
it "assigns default values" do
|
66
|
+
obj.bar.must_be_nil
|
67
|
+
obj.foo.must_equal 'I am foo'
|
68
|
+
end
|
69
|
+
|
70
|
+
it "can be assigned nil values" do
|
71
|
+
obj.number = nil
|
72
|
+
obj.number.must_be_nil
|
73
|
+
end
|
74
|
+
|
75
|
+
it "readonly attributes cannot be changed" do
|
76
|
+
lambda { obj.immutable = 'test' }.must_raise NoMethodError
|
77
|
+
obj.immutable.must_equal 'NO'
|
78
|
+
end
|
79
|
+
|
80
|
+
describe "property types" do
|
81
|
+
describe ":date" do
|
82
|
+
it "converts strings to dates" do
|
83
|
+
obj.date = "20100504"
|
84
|
+
obj.date.must_be_kind_of Date
|
85
|
+
end
|
86
|
+
|
87
|
+
it "accepts a Date" do
|
88
|
+
obj.date = Date.new 2010, 5, 4
|
89
|
+
obj.date.must_be_kind_of Date
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|
93
|
+
|
94
|
+
it "handles :integer" do
|
95
|
+
obj.number = "2"
|
96
|
+
obj.number.must_equal 2
|
97
|
+
end
|
98
|
+
|
99
|
+
describe ":boolean" do
|
100
|
+
it "returns true when assigning blank string" do
|
101
|
+
obj.doable = ''
|
102
|
+
obj.doable?.must_equal true
|
103
|
+
end
|
104
|
+
|
105
|
+
it "returns true when assigning nil" do
|
106
|
+
obj.doable = nil
|
107
|
+
obj.doable?.must_equal true
|
108
|
+
end
|
109
|
+
|
110
|
+
it "returns true when assigning 'J'" do
|
111
|
+
obj.doable = 'J'
|
112
|
+
obj.doable?.must_equal true
|
113
|
+
end
|
114
|
+
|
115
|
+
it "returns false" do
|
116
|
+
obj.doable = false
|
117
|
+
obj.doable?.must_equal false
|
118
|
+
|
119
|
+
obj.doable = 'N'
|
120
|
+
obj.doable?.must_equal false
|
121
|
+
end
|
122
|
+
|
123
|
+
end
|
124
|
+
|
125
|
+
end
|
126
|
+
|
127
|
+
it ".properties contains hash of options" do
|
128
|
+
Foo.properties[:foo].must_be_kind_of Hash
|
129
|
+
end
|
130
|
+
|
131
|
+
it "#properties contains hash of EfoNelfo::Property objects" do
|
132
|
+
Foo.new.properties[:foo].must_be_kind_of EfoNelfo::Property
|
133
|
+
end
|
134
|
+
|
135
|
+
it "#has_property? returns true when property exists" do
|
136
|
+
obj.has_property?(:foo).must_equal true
|
137
|
+
obj.has_property?(:bullshit).must_equal false
|
138
|
+
end
|
139
|
+
|
140
|
+
describe "#to_a" do
|
141
|
+
it "returns array of all attributes in correct order" do
|
142
|
+
obj.date = Date.new 2012, 5, 30
|
143
|
+
obj.number = 3
|
144
|
+
obj.doable = true
|
145
|
+
obj.to_a.must_equal ["I am foo", nil, "20120530", 3, "J", "NO"]
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
end
|
data/spec/property_spec.rb
CHANGED
@@ -1,144 +1,193 @@
|
|
1
|
-
# encoding: utf-8
|
2
1
|
require 'spec_helper'
|
3
2
|
|
4
3
|
describe EfoNelfo::Property do
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
4
|
+
subject { EfoNelfo::Property.new :foo, options }
|
5
|
+
let(:options) { nil }
|
6
|
+
|
7
|
+
describe "initializing with options" do
|
8
|
+
let(:options) { { type: :integer, limit: 15, decimals: 4, default: 5, required: true } }
|
9
|
+
|
10
|
+
it { subject.name.must_equal :foo }
|
11
|
+
it { subject.default.must_equal 5 }
|
12
|
+
it { subject.limit.must_equal 15 }
|
13
|
+
it { subject.type.must_equal :integer }
|
14
|
+
it { subject.required.must_equal true }
|
15
|
+
it { subject.decimals.must_equal 4 }
|
16
|
+
it { subject.value.must_equal 5 }
|
17
|
+
|
18
|
+
it "raises an error when assigning to unknown type" do
|
19
|
+
lambda {
|
20
|
+
EfoNelfo::Property.new :name, type: :get_lost
|
21
|
+
}.must_raise EfoNelfo::InvalidPropertyType
|
22
|
+
end
|
14
23
|
end
|
15
24
|
|
16
|
-
|
25
|
+
describe "initializing without options" do
|
26
|
+
let(:options) { nil }
|
17
27
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
}.must_raise EfoNelfo::DuplicateProperty
|
28
|
+
it { subject.name.must_equal :foo }
|
29
|
+
it { subject.default.must_be_nil }
|
30
|
+
it { subject.limit.must_equal 100 }
|
31
|
+
it { subject.type.must_equal :string }
|
32
|
+
it { subject.required.must_equal false }
|
33
|
+
it { subject.decimals.must_be_nil }
|
34
|
+
it { subject.value.must_be_nil }
|
26
35
|
end
|
27
36
|
|
28
|
-
|
29
|
-
|
30
|
-
class Bar
|
31
|
-
include EfoNelfo::Property
|
32
|
-
property :lame, funny: true
|
33
|
-
end
|
34
|
-
}.must_raise EfoNelfo::UnknownPropertyOption
|
35
|
-
end
|
37
|
+
describe "setter" do
|
38
|
+
let(:property) { EfoNelfo::Property.new :name, type: type }
|
36
39
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
40
|
+
describe "when type is string" do
|
41
|
+
let(:type) { :string }
|
42
|
+
it "is just a string" do
|
43
|
+
property.value = 'New value'
|
44
|
+
property.value.must_equal 'New value'
|
45
|
+
end
|
46
|
+
end
|
41
47
|
|
42
|
-
|
43
|
-
|
44
|
-
obj.foo.encoding.name.must_equal 'UTF-8'
|
45
|
-
end
|
48
|
+
describe "when type is boolean" do
|
49
|
+
let(:type) { :boolean }
|
46
50
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
obj.Føbar = 'Hacked'
|
51
|
-
obj.Føbar.must_equal 'Hacked'
|
52
|
-
obj.foo.must_equal 'Hacked'
|
53
|
-
end
|
51
|
+
it "accepts boolean" do
|
52
|
+
property.value = true
|
53
|
+
property.value.must_equal true
|
54
54
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
end
|
55
|
+
property.value = false
|
56
|
+
property.value.must_equal false
|
57
|
+
end
|
59
58
|
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
end
|
59
|
+
it "accepts J/N" do
|
60
|
+
property.value = 'J'
|
61
|
+
property.value.must_equal true
|
64
62
|
|
65
|
-
|
66
|
-
|
67
|
-
obj.immutable.must_equal 'NO'
|
68
|
-
end
|
63
|
+
property.value = 'j'
|
64
|
+
property.value.must_equal true
|
69
65
|
|
70
|
-
|
71
|
-
|
72
|
-
it "converts strings to dates" do
|
73
|
-
obj.date = "20100504"
|
74
|
-
obj.date.must_be_kind_of Date
|
66
|
+
property.value = 'N'
|
67
|
+
property.value.must_equal false
|
75
68
|
end
|
69
|
+
end
|
70
|
+
|
71
|
+
describe "when type is date" do
|
72
|
+
let(:type) { :date }
|
76
73
|
|
77
74
|
it "accepts a Date" do
|
78
|
-
|
79
|
-
|
75
|
+
property.value = Date.new(2013, 11, 21)
|
76
|
+
property.value.must_equal Date.new(2013, 11, 21)
|
80
77
|
end
|
81
78
|
|
82
|
-
|
79
|
+
it "accepts a string" do
|
80
|
+
property.value = "20131121"
|
81
|
+
property.value.must_equal Date.new(2013, 11, 21)
|
82
|
+
end
|
83
|
+
|
84
|
+
it "invalid dates becomes nil" do
|
85
|
+
property.value = 'invalid date'
|
86
|
+
property.value.must_be_nil
|
87
|
+
end
|
83
88
|
|
84
|
-
it "handles :integer" do
|
85
|
-
obj.number = "2"
|
86
|
-
obj.number.must_equal 2
|
87
89
|
end
|
88
90
|
|
89
|
-
describe "
|
90
|
-
|
91
|
-
obj.doable = ''
|
92
|
-
obj.doable?.must_equal true
|
93
|
-
end
|
91
|
+
describe "when type is integer" do
|
92
|
+
let(:type) { :integer }
|
94
93
|
|
95
|
-
it "
|
96
|
-
|
97
|
-
|
94
|
+
it "accepts a number" do
|
95
|
+
property.value = '2'
|
96
|
+
property.value.must_equal 2
|
98
97
|
end
|
99
98
|
|
100
|
-
it "
|
101
|
-
|
102
|
-
|
99
|
+
it "floats are rounded" do
|
100
|
+
property.value = 200.5
|
101
|
+
property.value.must_equal 200
|
103
102
|
end
|
104
103
|
|
105
|
-
it "
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
obj.doable = 'N'
|
110
|
-
obj.doable?.must_equal false
|
104
|
+
it "nil becomes nil" do
|
105
|
+
property.value = nil
|
106
|
+
property.value.must_be_nil
|
111
107
|
end
|
112
108
|
|
113
109
|
end
|
114
110
|
|
111
|
+
end
|
115
112
|
|
113
|
+
it "#value= is ignored if the property is readonly" do
|
114
|
+
prop = EfoNelfo::Property.new(:name, read_only: true, default: 'untouchable')
|
115
|
+
prop.value = 'hey'
|
116
|
+
prop.value.must_equal 'untouchable'
|
116
117
|
end
|
117
118
|
|
118
|
-
|
119
|
-
|
119
|
+
it "#value= converts booleans to proper value" do
|
120
|
+
prop = EfoNelfo::Property.new(:name, type: :boolean)
|
121
|
+
prop.value = 'J'
|
122
|
+
prop.value.must_equal true
|
120
123
|
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
+
prop.value = 'N'
|
125
|
+
prop.value.must_equal false
|
126
|
+
end
|
124
127
|
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
+
it "#to_f returns the value with decimals" do
|
129
|
+
prop = EfoNelfo::Property.new(:name, decimals: 4)
|
130
|
+
prop.value = 4000
|
131
|
+
prop.to_f.must_equal 0.4
|
132
|
+
end
|
133
|
+
|
134
|
+
it "#to_f doesn't fail when decimals is nil" do
|
135
|
+
subject.value = "100"
|
136
|
+
subject.to_f.must_equal 100.0
|
128
137
|
end
|
129
138
|
|
130
|
-
it "#
|
131
|
-
|
132
|
-
|
139
|
+
it "#to_f returns nil when value is NaN" do
|
140
|
+
subject.value = nil
|
141
|
+
subject.to_f.must_be_nil
|
133
142
|
end
|
134
143
|
|
135
|
-
describe "#
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
144
|
+
describe "#to_csv" do
|
145
|
+
def make_property(value, options={})
|
146
|
+
p = EfoNelfo::Property.new :name, options.merge(type: type)
|
147
|
+
p.value = value
|
148
|
+
p.to_csv
|
149
|
+
end
|
150
|
+
|
151
|
+
describe "type is :string" do
|
152
|
+
let(:type) { :string }
|
153
|
+
it { make_property("A string").must_equal "A string" }
|
154
|
+
it { make_property(nil).must_equal nil }
|
155
|
+
it { make_property("Sjøhest").encoding.must_equal Encoding::ISO_8859_1 }
|
141
156
|
end
|
157
|
+
|
158
|
+
describe "type is :integer" do
|
159
|
+
let(:type) { :integer }
|
160
|
+
it { make_property(2).must_equal 2 }
|
161
|
+
it { make_property(100, type: :integer, decimals: 4).must_equal 100 }
|
162
|
+
it { make_property(nil, type: :integer, decimals: 2).must_equal nil }
|
163
|
+
end
|
164
|
+
|
165
|
+
describe "type is :boolean" do
|
166
|
+
let(:type) { :boolean }
|
167
|
+
it { make_property(true).must_equal 'J' }
|
168
|
+
it { make_property(false).must_equal 'N' }
|
169
|
+
it { make_property(nil).must_equal 'J' }
|
170
|
+
end
|
171
|
+
|
172
|
+
describe "type is :date" do
|
173
|
+
let(:type) { :date }
|
174
|
+
it { make_property(Date.new(2013, 11, 21)).must_equal '20131121' }
|
175
|
+
it { make_property("20131121").must_equal "20131121" }
|
176
|
+
it { make_property("not a date").must_be_nil }
|
177
|
+
it { make_property(nil).must_be_nil }
|
178
|
+
end
|
179
|
+
|
142
180
|
end
|
143
181
|
|
182
|
+
it "raises error when initializing with unknown make_property" do
|
183
|
+
lambda {
|
184
|
+
EfoNelfo::Property.new :name, { foo: 'illegal' }
|
185
|
+
}.must_raise EfoNelfo::UnknownPropertyOption
|
186
|
+
end
|
187
|
+
|
188
|
+
it ".validate_options! raises error when passing invalid options" do
|
189
|
+
lambda {
|
190
|
+
EfoNelfo::Property.validate_options! foo: 'bar'
|
191
|
+
}.must_raise EfoNelfo::UnknownPropertyOption
|
192
|
+
end
|
144
193
|
end
|
data/spec/vl_spec.rb
CHANGED
@@ -4,6 +4,42 @@ describe EfoNelfo::V40::VL do
|
|
4
4
|
|
5
5
|
let(:vl) { EfoNelfo::V40::VL.new }
|
6
6
|
|
7
|
+
it "#weight returns the weight in gram" do
|
8
|
+
vl.info << { field: 'VEKT', value: 250 }
|
9
|
+
vl.weight.must_equal 250
|
10
|
+
end
|
11
|
+
|
12
|
+
it "#weight returns nil when weight is not present" do
|
13
|
+
vl.weight.must_be_nil
|
14
|
+
end
|
15
|
+
|
16
|
+
it "#dimension returns nil when dimension isn't present" do
|
17
|
+
vl.dimension.must_be_nil
|
18
|
+
end
|
19
|
+
|
20
|
+
it "#dimension returns the dimension of the object" do
|
21
|
+
vl.info << { field: 'VEKT', value: '10' }
|
22
|
+
vl.info << { field: 'DIMENSJON', value: '2x10x20' }
|
23
|
+
vl.dimension.must_equal '2x10x20'
|
24
|
+
end
|
25
|
+
|
26
|
+
it "#volume returns the volume" do
|
27
|
+
vl.info << { field: 'VOLUM', value: '1'}
|
28
|
+
vl.info << { field: 'DIMENSJON', value: '2x10x20' }
|
29
|
+
vl.volume.must_equal '1'
|
30
|
+
end
|
31
|
+
|
32
|
+
it "#volume returns nil when object doesn't have volume" do
|
33
|
+
vl.volume.must_be_nil
|
34
|
+
end
|
35
|
+
|
36
|
+
it "#urls returns list of vx lines containing a url" do
|
37
|
+
vl.info << { field: 'FOO', value: 'http://localhost/one?two'}
|
38
|
+
vl.info << { field: 'BAR', value: 'http://foo.example.com/one/two/three'}
|
39
|
+
vl.info << { field: 'FOO', value: 'http-not-a-url' }
|
40
|
+
vl.urls.must_equal [ 'http://localhost/one?two', 'http://foo.example.com/one/two/three']
|
41
|
+
end
|
42
|
+
|
7
43
|
describe "#images" do
|
8
44
|
it "returns nil when there are no images available" do
|
9
45
|
vl.images.must_be_empty
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: efo_nelfo
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Gudleik Rasch
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2013-
|
11
|
+
date: 2013-07-02 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -128,7 +128,9 @@ files:
|
|
128
128
|
- lib/efo_nelfo/has_many.rb
|
129
129
|
- lib/efo_nelfo/post_head_type.rb
|
130
130
|
- lib/efo_nelfo/post_type.rb
|
131
|
+
- lib/efo_nelfo/properties.rb
|
131
132
|
- lib/efo_nelfo/property.rb
|
133
|
+
- lib/efo_nelfo/property_types.rb
|
132
134
|
- lib/efo_nelfo/reader/csv.rb
|
133
135
|
- lib/efo_nelfo/v40/bh.rb
|
134
136
|
- lib/efo_nelfo/v40/bl.rb
|
@@ -144,6 +146,7 @@ files:
|
|
144
146
|
- spec/csv_writer_spec.rb
|
145
147
|
- spec/efo_nelfo_spec.rb
|
146
148
|
- spec/post_type_spec.rb
|
149
|
+
- spec/properties_spec.rb
|
147
150
|
- spec/property_spec.rb
|
148
151
|
- spec/samples/B028579.594.csv
|
149
152
|
- spec/samples/B028579.595.csv
|
@@ -179,7 +182,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
179
182
|
version: '0'
|
180
183
|
requirements: []
|
181
184
|
rubyforge_project:
|
182
|
-
rubygems_version: 2.0.
|
185
|
+
rubygems_version: 2.0.3
|
183
186
|
signing_key:
|
184
187
|
specification_version: 4
|
185
188
|
summary: Parser for EFONELFO format
|
@@ -188,6 +191,7 @@ test_files:
|
|
188
191
|
- spec/csv_writer_spec.rb
|
189
192
|
- spec/efo_nelfo_spec.rb
|
190
193
|
- spec/post_type_spec.rb
|
194
|
+
- spec/properties_spec.rb
|
191
195
|
- spec/property_spec.rb
|
192
196
|
- spec/samples/B028579.594.csv
|
193
197
|
- spec/samples/B028579.595.csv
|