litter 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: []