litter 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/bin/litter +23 -0
- data/lib/litter/config.rb +30 -0
- data/lib/litter/errors.rb +4 -0
- data/lib/litter/litter.rb +15 -0
- data/lib/litter/parsing/litter_file.rb +48 -0
- data/lib/litter/parsing/parser.rb +58 -0
- data/lib/litter/parsing/transform.rb +39 -0
- data/lib/litter/util/hash_deep_merge.rb +44 -0
- data/lib/litter/version.rb +3 -0
- metadata +155 -0
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,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
|
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: []
|