datanorm 0.0.1

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.
Files changed (37) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +154 -0
  3. data/lib/datanorm/document.rb +55 -0
  4. data/lib/datanorm/documents/assemble.rb +43 -0
  5. data/lib/datanorm/documents/assembles/price.rb +85 -0
  6. data/lib/datanorm/documents/assembles/product.rb +176 -0
  7. data/lib/datanorm/documents/preprocess.rb +36 -0
  8. data/lib/datanorm/documents/preprocesses/cache.rb +76 -0
  9. data/lib/datanorm/documents/preprocesses/process.rb +80 -0
  10. data/lib/datanorm/file.rb +65 -0
  11. data/lib/datanorm/header.rb +46 -0
  12. data/lib/datanorm/headers/v4/date.rb +39 -0
  13. data/lib/datanorm/headers/v4/version.rb +36 -0
  14. data/lib/datanorm/headers/v5/date.rb +25 -0
  15. data/lib/datanorm/headers/v5/version.rb +36 -0
  16. data/lib/datanorm/helpers/filename.rb +20 -0
  17. data/lib/datanorm/helpers/utf8.rb +20 -0
  18. data/lib/datanorm/lines/base.rb +67 -0
  19. data/lib/datanorm/lines/parse.rb +33 -0
  20. data/lib/datanorm/lines/v4/dimension.rb +44 -0
  21. data/lib/datanorm/lines/v4/extra.rb +55 -0
  22. data/lib/datanorm/lines/v4/parse.rb +42 -0
  23. data/lib/datanorm/lines/v4/price.rb +120 -0
  24. data/lib/datanorm/lines/v4/priceset.rb +42 -0
  25. data/lib/datanorm/lines/v4/product.rb +90 -0
  26. data/lib/datanorm/lines/v4/text.rb +31 -0
  27. data/lib/datanorm/lines/v5/dimension.rb +22 -0
  28. data/lib/datanorm/lines/v5/parse.rb +29 -0
  29. data/lib/datanorm/lines/v5/price.rb +27 -0
  30. data/lib/datanorm/lines/v5/product.rb +42 -0
  31. data/lib/datanorm/lines/v5/text.rb +30 -0
  32. data/lib/datanorm/logger.rb +15 -0
  33. data/lib/datanorm/logging.rb +27 -0
  34. data/lib/datanorm/progress.rb +26 -0
  35. data/lib/datanorm/version.rb +5 -0
  36. data/lib/datanorm.rb +49 -0
  37. metadata +158 -0
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datanorm
4
+ module Lines
5
+ module V4
6
+ # A `Priceset` record has one or multiple `Price` records.
7
+ # A price represents one price for one product.
8
+ #
9
+ # Examples:
10
+ # Q4058352208304;1;39000;1;0
11
+ # RG601315U1;1;2550;1;5500
12
+ # 100033162;1;28500;;
13
+ #
14
+ # [0] Artikelnummer
15
+ #
16
+ # [1] Preiskennzeichen (analogous to `Datanorm::Lines::V5::Product`)
17
+ # 1=wholesale (higher end-customer price)
18
+ # 2=retail (lower bulk price)
19
+ # 9=ask-for-price (only V5)
20
+ # Some documentation says this means 1=gross and 2=net but I cannot confirm that,
21
+ # the prices are always net prices and the retail/wholesale fits the bill.
22
+ #
23
+ # [2] Preis (6 Vorkomma, 2 Nachkommastellen)
24
+ # Price as Integer (6 digits before the comma, the last two digits represent the fraction)
25
+ #
26
+ # [3] Rabattkennzeichen (0=Rabattgruppe,1=Rabattsatz,2=Multi,3=Teuerungszuschlag)
27
+ # Discount type
28
+ # "0"=group [no change]
29
+ # "1"=rate [price - price * (factor/10000)]
30
+ # "2"=multiplier [price * (factor/1000)]
31
+ # "3"=surcharge [price + price * (factor/100)]
32
+ #
33
+ # [4] Rabatt
34
+ class Price
35
+ def initialize(columns:)
36
+ @columns = columns
37
+ end
38
+
39
+ # ------
40
+ # Basics
41
+ # ------
42
+
43
+ def id
44
+ ::Datanorm::Helpers::Utf8.call columns[0]
45
+ end
46
+
47
+ # -----
48
+ # Price
49
+ # -----
50
+
51
+ def retail?
52
+ columns[1] == '2'
53
+ end
54
+
55
+ def wholesale?
56
+ columns[1] == '1'
57
+ end
58
+
59
+ def cents
60
+ columns[2].to_i
61
+ end
62
+
63
+ def price
64
+ BigDecimal(cents) / 100
65
+ end
66
+
67
+ # --------
68
+ # Discount
69
+ # --------
70
+
71
+ # If this is true, then `cents` represents the final price.
72
+ def no_discount?
73
+ return true if columns[3] == '2'
74
+
75
+ # Fallback: If not defined, assume no discount.
76
+ columns[3].nil? || columns[3].empty?
77
+ end
78
+
79
+ # If this is true, a discount should be applied to `cents`.
80
+ def percentage_discount?
81
+ columns[3] == '1'
82
+ end
83
+
84
+ # How much of a discount do we get?
85
+ def discount_percentage_integer
86
+ columns[4]&.to_i
87
+ end
88
+
89
+ # -------
90
+ # Helpers
91
+ # -------
92
+
93
+ def to_s
94
+ "<Price #{as_json}>"
95
+ end
96
+
97
+ # We don't need the Product ID here.
98
+ # Our "parent" Priceset has the ID and all `Price` instances refer to the same product.
99
+ def as_json
100
+ {
101
+ is_retail: retail?,
102
+ is_wholesale: wholesale?,
103
+ is_no_discount: no_discount?,
104
+ is_percentage_discount: percentage_discount?,
105
+ discount_percentage: discount_percentage_integer,
106
+ cents:
107
+ }
108
+ end
109
+
110
+ def to_json(...)
111
+ as_json.to_json(...)
112
+ end
113
+
114
+ private
115
+
116
+ attr_reader :columns
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datanorm
4
+ module Lines
5
+ module V4
6
+ # rubocop:disable Layout/LineLength
7
+ # Represents one line of the Datanorm file that starts with "P".
8
+ # It contains price data for up to three individual products.
9
+ #
10
+ # Examples:
11
+ # V4
12
+ # P;A;Q4058352208304;1;39000;1;0;;;;;Q4058352208304;2;25521;;;;;;;Q4058352208403;1;42300;1;0;;;;;
13
+ # P;A;RG601315U1;1;2550;1;5500;;;;;RG601215U1;1;2130;1;5500;;;;;RG6211420U1;1;3210;1;5500;;;;;
14
+ # P;A;100033162;1;28500;;;;;;;;;;;;;;;;;;;;;;;;;
15
+ #
16
+ # rubocop:enable Layout/LineLength
17
+ class Priceset < ::Datanorm::Lines::Base
18
+ def to_s
19
+ "<Priceset with #{prices.size} prices>"
20
+ end
21
+
22
+ # Not available at this hierarchy level.
23
+ # The collection contains up to three product IDs.
24
+ def id
25
+ return prices.first.id if prices.size == 1
26
+
27
+ raise 'A Priceset with multiple products does not have one single ID'
28
+ end
29
+
30
+ def prices
31
+ [columns[2..6],
32
+ columns[11..15],
33
+ columns[20..24]].map do |set_columns|
34
+ next if set_columns[0].to_s == ''
35
+
36
+ ::Datanorm::Lines::V4::Price.new(columns: set_columns)
37
+ end.compact
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datanorm
4
+ module Lines
5
+ module V4
6
+ # Represents one line containting a product main name, quantity unit and price.
7
+ #
8
+ # Example:
9
+ # A;N;QBMK10208R;50;Brandmeldekabel rot;1;3;M;228313;2AED; ; ;
10
+ #
11
+ # [0] Satzartenkennzeichen "A"
12
+ # [1] Verarbeitungskennzeichen (N=Neuanlage, L=Löschung, A=Änderung, X=Artikelnummernänderung)
13
+ # [2] Artikelnummer
14
+ # [3] Textkennzeichen
15
+ # [4] Artikelbezeichnung 1 (Kurztextzeile 1)
16
+ # [5] Artikelbezeichnung 2 (Kurztextzeile 2)
17
+ #
18
+ # [6] Preiskennzeichen (see `Datanorm::Lines::V4::Price`)
19
+ #
20
+ # [7] Preiseinheit (0= per Mengeneinheit 1; 1=10, 2=100, 3=1000)
21
+ # [8] Mengeneinheit (Stk, m, lfm)
22
+ # [9] Preis (Wenn Hersteller die Preise mit Satzart "P" liefern, braucht hier kein Preis (0)
23
+ # eingetragen werden)
24
+ # [10] Rabattgruppe (Zur Ermittlung des Netto-Artikelpreises über die Rabattmatrix)
25
+ # [11] Hauptwarengruppe
26
+ # [12] Langtextschlüssel (Mit dem Langtextschlüssel wird ein Text aus mehreren Zeilen
27
+ # (Satzart T) an den Artikel gekettet.
28
+ class Product < ::Datanorm::Lines::Base
29
+ def to_s
30
+ "<Product #{as_json}>"
31
+ end
32
+
33
+ def id
34
+ ::Datanorm::Helpers::Utf8.call columns[2]
35
+ end
36
+
37
+ def text_id
38
+ ::Datanorm::Helpers::Utf8.call columns[12]
39
+ end
40
+
41
+ def retail_price?
42
+ columns[6] == '1'
43
+ end
44
+
45
+ def wholesale_price?
46
+ columns[6] == '2'
47
+ end
48
+
49
+ def cents
50
+ columns[9].to_i
51
+ end
52
+
53
+ def title
54
+ ::Datanorm::Helpers::Utf8.call columns[4..5].join(' ').strip
55
+ end
56
+
57
+ def quantity_unit
58
+ ::Datanorm::Helpers::Utf8.call columns[8]
59
+ end
60
+
61
+ def quantity
62
+ case columns[7].to_i
63
+ when 0 then 1
64
+ when 1 then 10
65
+ when 2 then 100
66
+ when 3 then 1000
67
+ end
68
+ end
69
+
70
+ def discount_group
71
+ ::Datanorm::Helpers::Utf8.call columns[9]
72
+ end
73
+
74
+ def as_json # rubocop:disable Metrics/MethodLength
75
+ {
76
+ id:,
77
+ text_id:,
78
+ is_retail_price: retail_price?,
79
+ is_wholesale_price: wholesale_price?,
80
+ cents:,
81
+ title:,
82
+ quantity_unit:,
83
+ quantity:,
84
+ discount_group:
85
+ }
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datanorm
4
+ module Lines
5
+ module V4
6
+ # Wraps a line of a DATANORM file that represents one line of a product description.
7
+ # Also known as Langtextsatz.
8
+ class Text < ::Datanorm::Lines::Base
9
+ def to_s
10
+ "TEXT [#{id}] <#{line_number}> #{content.encode('UTF-8', 'CP850').gsub("\n", '⏎')}"
11
+ end
12
+
13
+ def id
14
+ columns[2]
15
+ end
16
+
17
+ def line_number
18
+ columns[4].to_i
19
+ end
20
+
21
+ def content
22
+ [columns[6], columns[9]].join("\n").strip
23
+ end
24
+
25
+ def <=>(other)
26
+ line_number <=> other.line_number
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datanorm
4
+ module Lines
5
+ module V5
6
+ # Immediate product description texts. Should take precedence over Text records.
7
+ class Dimension < ::Datanorm::Lines::Base
8
+ def to_s
9
+ "[#{id}] DIMENSION-5 - #{columns}"
10
+ end
11
+
12
+ def id
13
+ encode columns[2]
14
+ end
15
+
16
+ def content
17
+ to_s
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datanorm
4
+ module Lines
5
+ module V5
6
+ # Converts one line of a Datanorm file to a Ruby object.
7
+ class Parse
8
+ include Calls
9
+
10
+ # Note that B-records in v4 contain data, but in V4, they are only DELETION notices.
11
+ CLASSES = {
12
+ 'A' => Datanorm::Lines::V5::Product,
13
+ 'T' => Datanorm::Lines::V5::Text,
14
+ 'D' => Datanorm::Lines::V5::Dimension,
15
+ 'P' => Datanorm::Lines::V5::Price
16
+ # 'C' => Datanorm::Lines::V5::Service
17
+ }.freeze
18
+
19
+ option :columns
20
+ option :source_line_number
21
+
22
+ def call
23
+ klass = CLASSES.fetch(columns.first[0], Datanorm::Lines::Base)
24
+ klass.new(columns:, source_line_number:)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datanorm
4
+ module Lines
5
+ module V5
6
+ # A price represents one price for one product.
7
+ #
8
+ # Examples:
9
+ # P;100033152;1;1;28000;06;;;;;;;;
10
+ # P;VSP-983-B;1;1;3410;BMT;
11
+ #
12
+ class Price
13
+ def to_s
14
+ "<Price id=#{id.inspect} #{discounts.join(' | ')}>"
15
+ end
16
+
17
+ def id
18
+ encode columns[2]
19
+ end
20
+
21
+ def cents
22
+ columns[4].to_i
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datanorm
4
+ module Lines
5
+ module V5
6
+ # Ruby wrapper for one A line of a Datanorm file.
7
+ class Product < ::Datanorm::Lines::Base
8
+ def to_s
9
+ "[#{id}] Product-5 #{item_title} - EUR #{price}"
10
+ end
11
+
12
+ def id
13
+ columns[2]
14
+ end
15
+
16
+ def text_id
17
+ columns[23]
18
+ end
19
+
20
+ def cents
21
+ columns[8].to_i
22
+ end
23
+
24
+ def title
25
+ columns[3..4].join(' ').strip
26
+ end
27
+
28
+ def quantity_unit
29
+ columns[5]
30
+ end
31
+
32
+ def quantity
33
+ columns[6].to_i.nonzero?
34
+ end
35
+
36
+ def as_json
37
+ { id:, text_id:, cents:, title:, quantity_unit:, quantity: }
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datanorm
4
+ module Lines
5
+ module V5
6
+ # One line of the Datanorm file starting with T
7
+ class Text < ::Datanorm::Lines::Base
8
+ def to_s
9
+ "[#{id}] TEXT-5 #{line_number} #{content}"
10
+ end
11
+
12
+ def id
13
+ columns[2]
14
+ end
15
+
16
+ def line_number
17
+ columns[4].to_i
18
+ end
19
+
20
+ def content
21
+ columns[5]
22
+ end
23
+
24
+ def <=>(other)
25
+ line_number <=> other.line_number
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ # A Rubygem to parse DATANORM files from the 90s.
4
+ module Datanorm
5
+ # Helper to create a STDOUT logger
6
+ def self.logger
7
+ return @logger if defined?(@logger)
8
+
9
+ @logger = ::Logger.new($stdout)
10
+ @logger.formatter = proc do |severity, _, progname, message|
11
+ [severity.rjust(5), progname, '-', message, "\n"].join(' ')
12
+ end
13
+ @logger
14
+ end
15
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datanorm
4
+ # Adds a convenience method for debug logging.
5
+ module Logging
6
+ def self.included(base)
7
+ base.extend ClassMethods
8
+ end
9
+
10
+ # Log helper on class level.
11
+ module ClassMethods
12
+ def log(&)
13
+ return unless ENV['DEBUG_DATANORM']
14
+
15
+ ::Datanorm.logger&.debug(to_s, &)
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def log(&)
22
+ return unless ENV['DEBUG_DATANORM']
23
+
24
+ ::Datanorm.logger&.debug(self.class.to_s, &)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datanorm
4
+ # Represents how much processing elapsed and how much is left to be done.
5
+ class Progress
6
+ include Datanorm::Logging
7
+
8
+ attr_accessor :title, :current, :total
9
+
10
+ def increment!
11
+ self.current += 1
12
+ end
13
+
14
+ def to_s
15
+ "#{percentage}% (#{title} #{current}/#{total})"
16
+ end
17
+
18
+ def significant?
19
+ (current % 50_000).zero?
20
+ end
21
+
22
+ def percentage
23
+ ((current.to_f / total) * 100).round(1)
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datanorm
4
+ Version = ::Data.define(:number, :four?, :five?)
5
+ end
data/lib/datanorm.rb ADDED
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'base64'
4
+ require 'bigdecimal'
5
+ require 'calls'
6
+ require 'csv'
7
+ require 'json'
8
+ require 'logger'
9
+ require 'securerandom'
10
+ require 'tmpdir'
11
+ require 'tron'
12
+
13
+ require 'datanorm/logger'
14
+ require 'datanorm/logging'
15
+ require 'datanorm/helpers/utf8'
16
+ require 'datanorm/helpers/filename'
17
+
18
+ require 'datanorm/headers/v4/date'
19
+ require 'datanorm/headers/v4/version'
20
+ require 'datanorm/headers/v5/date'
21
+ require 'datanorm/headers/v5/version'
22
+
23
+ require 'datanorm/lines/parse'
24
+ require 'datanorm/lines/base'
25
+ require 'datanorm/lines/v4/product'
26
+ require 'datanorm/lines/v4/extra'
27
+ require 'datanorm/lines/v4/priceset'
28
+ require 'datanorm/lines/v4/price'
29
+ require 'datanorm/lines/v4/dimension'
30
+ require 'datanorm/lines/v4/text'
31
+ require 'datanorm/lines/v5/product'
32
+ require 'datanorm/lines/v5/price'
33
+ require 'datanorm/lines/v5/text'
34
+ require 'datanorm/lines/v5/dimension'
35
+
36
+ require 'datanorm/lines/v4/parse'
37
+ require 'datanorm/lines/v5/parse'
38
+
39
+ require 'datanorm/documents/preprocess'
40
+ require 'datanorm/documents/preprocesses/cache'
41
+ require 'datanorm/documents/preprocesses/process'
42
+ require 'datanorm/documents/assembles/product'
43
+ require 'datanorm/documents/assembles/price'
44
+ require 'datanorm/documents/assemble'
45
+ require 'datanorm/progress'
46
+ require 'datanorm/document'
47
+ require 'datanorm/file'
48
+ require 'datanorm/header'
49
+ require 'datanorm/version'