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 +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: []
|