litter 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a6adfd002bc8d05155ebf3c70440cc438e91784d71f48ebff1fe0d4e63085572
4
+ data.tar.gz: 558de515de4733799ef558f38e0f3d69d12768b3bbc728d015317d151d12fee8
5
+ SHA512:
6
+ metadata.gz: e598c143cb95112c5701d278117101815757faf8e372733467339dd77e2a16ffde2be378afbbc121f43bc713150dbe7f17e2bbde7bb6fdacea29b5225c9b7314
7
+ data.tar.gz: cfd284c206c0d12aee126563e03328acf3074d8c2dd8e6cd6f26909c596243ddb16b5e3315a552cd86d1abb69fc206b9cbc3d30280ffb5409ce105e105b7c158
data/bin/litter ADDED
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Parses a litter file and shows stats.
4
+ #
5
+ # Usage:
6
+ # Run on the command line:
7
+ # litter "<file path>"
8
+ #
9
+ # Example:
10
+ # litter ~/trash.txt
11
+
12
+ require_relative "../lib/litter/litter"
13
+ require "amazing_print"
14
+
15
+ path = ARGV[0]
16
+ unless path
17
+ raise ArgumentError, "File path argument required."
18
+ end
19
+
20
+ # TODO show stats instead of the raw parser output.
21
+ parsed = Litter.parse(path)
22
+
23
+ ap parsed
@@ -0,0 +1,30 @@
1
+ module Litter
2
+ # Builds a hash config.
3
+ class Config
4
+ using Util::HashDeepMerge
5
+
6
+ attr_reader :hash
7
+
8
+ # @param custom_config [Hash] a custom config which overrides the defaults,
9
+ # e.g. { parser: { date_sep: '-' } }
10
+ def initialize(custom_config = {})
11
+ @hash = default_config.deep_merge(custom_config)
12
+ end
13
+
14
+ private
15
+
16
+ # The default config, excluding Regex config (see further down).
17
+ # @return [Hash]
18
+ def default_config
19
+ {
20
+ parser:
21
+ {
22
+ date_sep: '/',
23
+ loc_char: '@',
24
+ qty_char: '*',
25
+ tag_char: '#',
26
+ },
27
+ }
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,4 @@
1
+ module Litter
2
+ # Means there was a problem accessing a file.
3
+ class FileError < StandardError; end
4
+ end
@@ -0,0 +1,15 @@
1
+ require "parslet"
2
+ require "date"
3
+
4
+ require_relative "util/hash_deep_merge"
5
+ require_relative "config"
6
+ require_relative "errors"
7
+ require_relative "parsing/litter_file"
8
+
9
+ module Litter
10
+ # Parses a file. See Parser::LitterFile#initialize and #parse for details.
11
+ def self.parse(...)
12
+ file = Parsing::LitterFile.new(...)
13
+ file.parse
14
+ end
15
+ end
@@ -0,0 +1,48 @@
1
+ require_relative "parser"
2
+ require_relative "transform"
3
+
4
+ module Litter
5
+ module Parsing
6
+ class LitterFile
7
+ attr_reader :string, :config
8
+
9
+ # @param path [String] the path of the file to be parsed; if nil, uses file instead.
10
+ # @param path [File] the file to be parsed.
11
+ # @param config [Hash] custom config that overrides defaults in config.rb.
12
+ def initialize(path = nil, file: nil, config: {})
13
+ validate_path_or_file(path, file)
14
+
15
+ @string = path ? File.read(path) : file.read
16
+ @config = Config.new(config || {}).hash
17
+ end
18
+
19
+ # Parses the file.
20
+ # @return [Hash] a hash of items and their finds; see parse_test.rb for an example.
21
+ def parse
22
+ parsed = Parser.new(config[:parser]).parse(string)
23
+ hashes = Transform.new.apply(parsed)
24
+
25
+ hashes
26
+ end
27
+
28
+ private
29
+
30
+ # Checks on the given file path.
31
+ # @raise [FileError] if the given path is invalid.
32
+ # @raise [ArgumentError] if both path and file are nil.
33
+ def validate_path_or_file(path, file)
34
+ return true if file && file.respond_to?(:read)
35
+
36
+ if path
37
+ if !File.exist?(path)
38
+ raise FileError, "File not found! #{path}"
39
+ elsif File.directory?(path)
40
+ raise FileError, "A file is expected, but the path given is a directory: #{path}"
41
+ end
42
+ else
43
+ raise ArgumentError, "Either a string or a file path must be provided."
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,58 @@
1
+ module Litter
2
+ module Parsing
3
+ # Example output from the example in parse_test.rb.
4
+ # Note: what looks like strings followed by "@" are actually Parslet::Slice.
5
+ #
6
+ # [{:date=>"2023/03/08"@0,
7
+ # :location=>"west wildwood"@12,
8
+ # :items=>
9
+ # [{:name=>"car mat"@26},
10
+ # {:name=>"folding chair "@34, :quantity=>"2"@49, :tag=>"kept"@52},
11
+ # {:name=>"concrete-filled bucket "@57, :location=>"bridge"@81}]},
12
+ # {:date=>"2023/4/1"@89,
13
+ # :items=>
14
+ # [{:name=>"concrete-filled bucket "@98, :quantity=>"2"@122, :location=>"west wildwood "@125, :tag=>"TODO"@140},
15
+ # {:name=>"shopping cart "@145, :location=>"bridge"@160},
16
+ # {:name=>"car mat "@167, :tag=>"kept"@176}]}]
17
+ #
18
+ class Parser < Parslet::Parser
19
+ attr_reader :config
20
+
21
+ # @param config [Hash] the parser config, such as Config#default_config[:parser]
22
+ def initialize(config)
23
+ @config = config
24
+ super()
25
+ end
26
+
27
+ rule(:newline) { str("\n") }
28
+ rule(:space) { match('\s').repeat(1) }
29
+ rule(:date_sep) { str(config.fetch(:date_sep)) }
30
+ rule(:loc_char) { str(config.fetch(:loc_char)) }
31
+ rule(:qty_char) { str(config.fetch(:qty_char)) }
32
+ rule(:tag_char) { str(config.fetch(:tag_char)) }
33
+ rule(:not_tag) { match( "[^\\n\\#{config.fetch(:tag_char)}]") }
34
+ rule(:not_special) {
35
+ match("[^\\n\\#{config[:qty_char]}\\#{config[:loc_char]}\\#{config[:tag_char]}]")
36
+ }
37
+
38
+ rule(:year) { match('[0-9]').repeat(4,4) }
39
+ rule(:day_or_month) { match('[0-9]').repeat(1,2) }
40
+ rule(:date) { (year >> date_sep >> day_or_month >> date_sep >> day_or_month).as(:date) }
41
+
42
+ rule(:location) { space.maybe >> loc_char >> not_tag.repeat(1).as(:location)}
43
+ rule(:date_line) { date >> location.maybe >> newline }
44
+
45
+ rule(:name) { not_special.repeat(1).as(:name) }
46
+ rule(:quantity) { qty_char >> match('[0-9]').repeat(1).as(:quantity) }
47
+ rule(:tag) { space.maybe >> tag_char >> match('[^\n]').repeat(1).as(:tag) }
48
+ rule(:item) { name >> quantity.maybe >> location.maybe >> tag.maybe }
49
+
50
+ rule(:items) { (item >> newline).repeat.as(:items) }
51
+ rule(:entry) { date_line >> items >> newline.repeat }
52
+
53
+ rule(:document) { entry.repeat }
54
+
55
+ root :document
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,39 @@
1
+ module Litter
2
+ module Parsing
3
+ class Transform
4
+ # Transforms output from Parser (an array of entry hashes) into a hash of
5
+ # arrays containing item finds.
6
+ # @param parsed [Array<Hash>] output from Parser.
7
+ # @return [Hash] a hash of items and their finds; see parse_test.rb for an example.
8
+ def apply(parsed)
9
+ all_items = {}
10
+
11
+ parsed.each do |entry|
12
+ extract_entry_into_all_items(entry, all_items)
13
+ end
14
+
15
+ all_items
16
+ end
17
+
18
+ private
19
+
20
+ # Adds the given entry's item finds to the given hash of all items.
21
+ # @param entry [Hash] an entry (items found on a date).
22
+ # @param all_items [Hash] all items extracted from entries so far.
23
+ def extract_entry_into_all_items(entry, all_items)
24
+ entry[:items].each do |item|
25
+ name = item[:name].to_s.strip
26
+ date = Date.parse(entry[:date])
27
+ quantity = Integer(item[:quantity].to_s, exception: false) || 1
28
+ location = (item[:location] || entry[:location])&.to_s&.strip
29
+ tag = item[:tag]&.to_s
30
+
31
+ all_items[name] ||= []
32
+ quantity.times do
33
+ all_items[name] << { date:, location:, tag: }
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,44 @@
1
+ module Litter
2
+ module Util
3
+ # Modified from active_support/core_ext/hash/deep_merge
4
+ # https://github.com/rails/rails/blob/main/activesupport/lib/active_support/core_ext/hash/deep_merge.rb
5
+ #
6
+ # This deep_merge also iterates through arrays of hashes and merges them.
7
+ module HashDeepMerge
8
+ refine Hash do
9
+ def deep_merge(other_hash, &block)
10
+ dup.deep_merge!(other_hash, &block)
11
+ end
12
+
13
+ def deep_merge!(other_hash, &block)
14
+ merge!(other_hash) do |key, this_val, other_val|
15
+ if this_val.is_a?(Hash) && other_val.is_a?(Hash)
16
+ this_val.deep_merge(other_val, &block)
17
+ # I added this part for merging values that are arrays of hashes.
18
+ elsif this_val.is_a?(Array) && other_val.is_a?(Array) &&
19
+ this_val.all? { |el| el.is_a?(Hash) } &&
20
+ other_val.all? { |el| el.is_a?(Hash) }
21
+ zip =
22
+ if other_val.length >= this_val.length
23
+ other_val.zip(this_val)
24
+ else
25
+ this_val.zip(other_val).map(&:reverse)
26
+ end
27
+ zip.map { |other_el, this_el|
28
+ if this_el.nil?
29
+ other_el
30
+ else
31
+ this_el.deep_merge(other_el || {}, &block)
32
+ end
33
+ }
34
+ elsif block_given?
35
+ block.call(key, this_val, other_val)
36
+ else
37
+ other_val
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,3 @@
1
+ module Litter
2
+ VERSION = "0.1.0"
3
+ end
metadata ADDED
@@ -0,0 +1,155 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: litter
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Felipe Vogel
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-03-15 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: parslet
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: debug
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: minitest
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: minitest-reporters
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: pretty-diffs
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: amazing_print
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rubycritic
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ description:
112
+ email:
113
+ - fps.vogel@gmail.com
114
+ executables:
115
+ - litter
116
+ extensions: []
117
+ extra_rdoc_files: []
118
+ files:
119
+ - bin/litter
120
+ - lib/litter/config.rb
121
+ - lib/litter/errors.rb
122
+ - lib/litter/litter.rb
123
+ - lib/litter/parsing/litter_file.rb
124
+ - lib/litter/parsing/parser.rb
125
+ - lib/litter/parsing/transform.rb
126
+ - lib/litter/util/hash_deep_merge.rb
127
+ - lib/litter/version.rb
128
+ homepage: https://github.com/fpsvogel/litter
129
+ licenses:
130
+ - MIT
131
+ metadata:
132
+ allowed_push_host: https://rubygems.org
133
+ homepage_uri: https://github.com/fpsvogel/litter
134
+ source_code_uri: https://github.com/fpsvogel/litter
135
+ changelog_uri: https://github.com/fpsvogel/litter/blob/master/CHANGELOG.md
136
+ post_install_message:
137
+ rdoc_options: []
138
+ require_paths:
139
+ - lib
140
+ required_ruby_version: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - ">="
143
+ - !ruby/object:Gem::Version
144
+ version: 3.0.0
145
+ required_rubygems_version: !ruby/object:Gem::Requirement
146
+ requirements:
147
+ - - ">="
148
+ - !ruby/object:Gem::Version
149
+ version: '0'
150
+ requirements: []
151
+ rubygems_version: 3.4.7
152
+ signing_key:
153
+ specification_version: 4
154
+ summary: Parses a log of collected litter. For avid trash-picker-uppers.
155
+ test_files: []