scheduled-format 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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: c1fcfd8366043e95ad3620345cc368848c36e2b08b3c3671d3189f19696c7030
4
+ data.tar.gz: efe64d9385bbd041ac75ac18c10d094b73e490c038b0c742e554a0b3b8f10ecf
5
+ SHA512:
6
+ metadata.gz: 8b922da0a4298956a10e7fa8ca89b11ef5e945667bb6bac19d1ed3e9a3538402184789f35002e8b7e3a4735e074f39d80f629b433bd4ab363a264b9fce111619
7
+ data.tar.gz: 905a9715919ea3ff1dddbcd2adb3f11f076d06eb55351e5fd41350bd9e6ee664efd3325c014e3abb897d1170de1eebbe8b738feed84066fafd12ad8aa750f1ae
@@ -0,0 +1,63 @@
1
+ # About
2
+
3
+ [![Gem version][GV img]][Gem version]
4
+ [![Build status][BS img]][Build status]
5
+ [![Coverage status][CS img]][Coverage status]
6
+ [![CodeClimate status][CC img]][CodeClimate status]
7
+ [![YARD documentation][YD img]][YARD documentation]
8
+
9
+ This format is used to store **scheduled tasks**: tasks that will be done later
10
+ or in a certain context.
11
+
12
+ # API
13
+
14
+ ```ruby
15
+ require 'import'
16
+
17
+ simple_format = import('simple-format')
18
+ simple_format.parse(File.read('tasks.todo'))
19
+ ```
20
+
21
+ # Format
22
+
23
+ ```
24
+ Tomorrow
25
+ - Buy milk. #errands
26
+ - [9:20] Call with Mike.
27
+
28
+ Prague
29
+ - Pick up my shoes. #errands
30
+ ```
31
+
32
+ # Currently unsupported
33
+
34
+ - **Labels**. Labels allow us to match tasks with _named_ time frames.
35
+ See [#8](https://github.com/botanicus/now-task-manager/issues/8).
36
+
37
+ ```
38
+ - ADM: Catch up with Eva.
39
+ ```
40
+
41
+ # Intentionally unsupported
42
+
43
+ - **Comments**. I want to keep the format simple and the task file small.
44
+ Every time there was something like comments, the file bloated uncontrollably.
45
+ - **Task formatting**. Task is a string, it doesn't recognise any structures within.
46
+ Therefore, anything you can fit in to a line will be the task body. So you can
47
+ put anything that {Pomodoro::Formats::Today} supports such as scheduled times
48
+ and tags.
49
+
50
+ _For more details about the format see
51
+ [parser_spec.rb](https://github.com/botanicus/scheduled-format/blob/master/spec/scheduled-format/parser/parser_spec.rb)._
52
+
53
+ [Gem version]: https://rubygems.org/gems/scheduled-format
54
+ [Build status]: https://travis-ci.org/botanicus/scheduled-format
55
+ [Coverage status]: https://coveralls.io/github/botanicus/scheduled-format
56
+ [CodeClimate status]: https://codeclimate.com/github/botanicus/scheduled-format/maintainability
57
+ [YARD documentation]: http://www.rubydoc.info/github/botanicus/scheduled-format/master
58
+
59
+ [GV img]: https://badge.fury.io/rb/scheduled-format.svg
60
+ [BS img]: https://travis-ci.org/botanicus/scheduled-format.svg?branch=master
61
+ [CS img]: https://img.shields.io/coveralls/botanicus/scheduled-format.svg
62
+ [CC img]: https://api.codeclimate.com/v1/badges/a99a88d28ad37a79dbf6/maintainability
63
+ [YD img]: http://img.shields.io/badge/yard-docs-blue.svg
@@ -0,0 +1,37 @@
1
+ require 'parslet'
2
+ require 'parslet/convenience' # parse_with_debug
3
+
4
+ Parser = import('scheduled-format/parser/parser')
5
+ Transformer = import('scheduled-format/parser/transformer')
6
+ TaskList = import('scheduled-format/task_list')
7
+
8
+ # {include:file:doc/formats/scheduled.md}
9
+ # The entry point method for parsing this format.
10
+ #
11
+ # @param string [String] string in the scheduled task list format
12
+ # @return [TaskList, nil]
13
+ #
14
+ # @example
15
+ # scheduled_format = import('scheduled-format')
16
+ #
17
+ # task_list = scheduled_format.parse <<-EOF.gsub(/^\s*/, '')
18
+ # Tomorrow
19
+ # - Buy milk. #errands
20
+ # - [9:20] Call with Mike.
21
+ #
22
+ # Prague
23
+ # - Pick up my shoes. #errands
24
+ # EOF
25
+ # @since 0.2
26
+ def exports.parse(string_or_io)
27
+ string = string_or_io.respond_to?(:read) ? string_or_io.read : string_or_io
28
+ tree = Parser.new.parse_with_debug(string)
29
+ nodes = Transformer.new.apply(tree)
30
+ TaskList.new(nodes.empty? ? Array.new : nodes)
31
+ end
32
+
33
+ export parser: Parser,
34
+ transformer: Transformer,
35
+ task: import('scheduled-format/task'),
36
+ task_list: TaskList,
37
+ task_group: import('scheduled-format/task_group')
@@ -0,0 +1,46 @@
1
+ require 'parslet'
2
+
3
+ # @api private
4
+ class Parser < Parslet::Parser
5
+ rule(:hour_strict) {
6
+ (match['\d'].repeat(1) >> str(':') >> match['\d'].repeat(2, 2)).as(:hour)
7
+ }
8
+
9
+ rule(:start_time) {
10
+ str('[') >> hour_strict.as(:start_time) >> str(']') >> str(' ').maybe
11
+ }
12
+
13
+ rule(:time_frame) {
14
+ # TODO: (hour_strict.absent? >> match['^\]\n'] >> any)
15
+ str('[') >> match['\w '].repeat.as(:str) >> str(']') >> str(' ').maybe
16
+ }
17
+
18
+ rule(:header) {
19
+ # match['^\n'].repeat # This makes it hang!
20
+ (str("\n").absent? >> any).repeat(1).as(:str) >> str("\n")
21
+ }
22
+
23
+ rule(:task_body) do
24
+ (match['#\n'].absent? >> any).repeat.as(:str)
25
+ end
26
+
27
+ rule(:tag) do
28
+ str('#') >> match['^\s'].repeat.as(:str) >> str(' ').maybe
29
+ end
30
+
31
+ rule(:task) {
32
+ str('- ') >> (time_frame.as(:time_frame).maybe >> start_time.maybe >> task_body.as(:body) >> tag.as(:tag).repeat.as(:tags).maybe).as(:task) >> str("\n").repeat
33
+ }
34
+
35
+ rule(:task_group) {
36
+ (header.as(:header) >> task.repeat.as(:tasks)).as(:task_group)
37
+ }
38
+
39
+ rule(:task_groups) {
40
+ task_group.repeat(0)
41
+ }
42
+
43
+ root(:task_groups)
44
+ end
45
+
46
+ export { Parser }
@@ -0,0 +1,26 @@
1
+ require 'parslet'
2
+ require 'refined-refinements/hour'
3
+
4
+ Task = import('scheduled-format/task')
5
+ TaskGroup = import('scheduled-format/task_group')
6
+
7
+ # @api private
8
+ class Transformer < Parslet::Transform
9
+ rule(str: simple(:slice)) { slice.to_s.strip }
10
+
11
+ rule(tag: simple(:slice)) { slice.to_sym }
12
+
13
+ rule(hour: simple(:hour_string)) do
14
+ Hour.parse(hour_string.to_s)
15
+ end
16
+
17
+ rule(task: subtree(:data)) do
18
+ Task.new(**data)
19
+ end
20
+
21
+ rule(task_group: subtree(:data)) {
22
+ TaskGroup.new(**data)
23
+ }
24
+ end
25
+
26
+ export { Transformer }
@@ -0,0 +1,39 @@
1
+ class Task
2
+ attr_reader :body, :time_frame, :start_time, :tags
3
+
4
+ # Create a new scheduled task.
5
+ #
6
+ # @param body [String] the task description.
7
+ # @param start_time [Hour] when the task starts.
8
+ # @param time_frame [String] name, initials or an abbreviation of a time frame
9
+ # to which the task will be scheduled.
10
+ # @param tags [Array<Symbol>] list of tags.
11
+ def initialize(body:, time_frame: nil, start_time: nil, tags: Array.new)
12
+ @body, @time_frame, @start_time, @tags = body, time_frame, start_time, tags
13
+
14
+ if body.empty?
15
+ raise ArgumentError.new("Body cannot be empty.")
16
+ end
17
+
18
+ if start_time && ! start_time.is_a?(Hour)
19
+ raise ArgumentError.new("Hour instance was expected, got #{start_time.class}")
20
+ end
21
+
22
+ if ! tags.empty? && tags.any? { |tag| ! tag.is_a?(Symbol) }
23
+ raise ArgumentError.new("Tags are supposed to be an array of symbols.")
24
+ end
25
+ end
26
+
27
+ # Format task in the {Pomodoro::Formats::Scheduled scheduled task list format}.
28
+ # @since 0.2
29
+ def to_s
30
+ [
31
+ '-',
32
+ ("[#{@time_frame}]" if @time_frame),
33
+ ("[#{@start_time}]" if @start_time),
34
+ "#{@body}", *@tags.map { |tag| "##{tag}"}
35
+ ].compact.join(' ')
36
+ end
37
+ end
38
+
39
+ export { Task }
@@ -0,0 +1,101 @@
1
+ require 'date'
2
+
3
+ class TaskGroup
4
+ # @since 0.2
5
+ attr_reader :header, :tasks
6
+
7
+ # @param header [String] header of the task group.
8
+ # @param tasks [Array<String>] tasks of the group.
9
+ # @since 0.2
10
+ #
11
+ # @example
12
+ # TaskGroup = import('scheduled-format/task_group')
13
+ #
14
+ # tasks = ['Buy milk. #errands', '[9:20] Call with Mike.']
15
+ # group = TaskGroup.new(header: 'Tomorrow', tasks: tasks)
16
+ def initialize(header:, tasks: Array.new)
17
+ @header, @tasks = header, tasks
18
+ if tasks.any? { |task| ! task.is_a?(Task) }
19
+ raise ArgumentError.new("Task objects expected.")
20
+ end
21
+ end
22
+
23
+ # Add a task to the task group.
24
+ #
25
+ # @since 0.2
26
+ def <<(task)
27
+ unless task.is_a?(Task)
28
+ raise ArgumentError.new("Task expected, got #{task.class}.")
29
+ end
30
+
31
+ @tasks << task unless @tasks.map(&:to_s).include?(task.to_s)
32
+ end
33
+
34
+ # Remove a task from the task group.
35
+ #
36
+ # @since 0.2
37
+ def delete(task)
38
+ unless task.is_a?(Task)
39
+ raise ArgumentError.new("Task expected, got #{task.class}.")
40
+ end
41
+
42
+ @tasks.delete_if { |t2| t2.to_s == task.to_s }
43
+ end
44
+
45
+ # Return a scheduled task list formatted string.
46
+ #
47
+ # @since 0.2
48
+ def to_s
49
+ [@header, @tasks.map(&:to_s), nil].flatten.join("\n")
50
+ end
51
+
52
+ def save(path)
53
+ data = self.to_s
54
+ File.open(path, 'w:utf-8') do |file|
55
+ file.puts(data)
56
+ end
57
+ end
58
+
59
+ # TODO: Next Monday
60
+ # NOTE: For parsing we don't use %-d etc, only %d.
61
+ DATE_FORMATS = {
62
+ '%d/%m' => :next_month, # 1/1
63
+ '%d/%m/%Y' => nil, # 1/1/2018
64
+ '%A %d/%m' => :next_week, # Monday 1/1 Note: Higher specifity has to come first.
65
+ '%A' => :next_week # Monday
66
+ }
67
+
68
+ # labels = ['Tomorrow', date.strftime('%A'), date.strftime('%-d/%m'), date.strftime('%-d/%m/%Y')]
69
+ def scheduled_date
70
+ return Date.today if @header == 'Today' # Change tomorrow to Today if you're generating it in the morning.
71
+ return Date.today + 1 if @header == 'Tomorrow'
72
+ parse_date_in_the_future(@header)
73
+ end
74
+
75
+ def tomorrow?
76
+ self.scheduled_date == Date.today + 1
77
+ end
78
+
79
+ private
80
+
81
+ # TODO: We need base_date, Date.today wouldn't cut it if we run "now g +3".
82
+ def parse_date_in_the_future(header)
83
+ DATE_FORMATS.each do |format, adjustment_method|
84
+ begin
85
+ date = Date.strptime(header, format)
86
+ date.define_singleton_method(:next_week) { self + 7 } # TODO: DataExts, extract it from commands.rb.
87
+ return ensure_in_the_future(date, adjustment_method)
88
+ rescue ArgumentError
89
+ end
90
+ end
91
+
92
+ return nil
93
+ end
94
+
95
+ def ensure_in_the_future(date, adjustment_method)
96
+ return date if adjustment_method.nil? || Date.today <= date
97
+ date.send(adjustment_method)
98
+ end
99
+ end
100
+
101
+ export { TaskGroup }
@@ -0,0 +1,106 @@
1
+ class TaskList
2
+ include Enumerable
3
+
4
+ # List of {TaskGroup task groups}. Or more precisely objects responding to `#header` and `#tasks`.
5
+ # @since 0.2
6
+ attr_reader :data
7
+
8
+ # @param [Array<TaskGroup>] data List of task groups.
9
+ # Or more precisely objects responding to `#header` and `#tasks`.
10
+ # @raise [ArgumentError] if data is not an array or if its content doesn't
11
+ # respond to `#header` and `#tasks`.
12
+ #
13
+ # @example
14
+ # TaskGroup, TaskList = import('scheduled-format').grab(:TaskGroup, :TaskList)
15
+ #
16
+ # tasks = ['Buy milk. #errands', '[9:20] Call with Mike.']
17
+ # group = TaskGroup.new(header: 'Tomorrow', tasks: tasks)
18
+ # list = TaskList.new([group])
19
+ # @since 0.2
20
+ def initialize(data)
21
+ @data = data
22
+
23
+ unless data.is_a?(Array) && data.all? { |item| item.respond_to?(:header) && item.respond_to?(:tasks) }
24
+ raise ArgumentError.new("Data is supposed to be an array of TaskGroup instances.")
25
+ end
26
+ end
27
+
28
+ # Find a task group that matches given header.
29
+ #
30
+ # @return [TaskGroup, nil] matching the header.
31
+ # @since 0.2
32
+ #
33
+ # @example
34
+ # # Using the code from the initialiser.
35
+ # list['Tomorrow']
36
+ def [](header)
37
+ @data.find do |task_group|
38
+ task_group.header == header
39
+ end
40
+ end
41
+
42
+ # Add a task group onto the task list.
43
+ #
44
+ # @raise [ArgumentError] if the task group is already in the list.
45
+ # @param [TaskGroup] task_group the task group.
46
+ # @since 0.2
47
+ def <<(task_group)
48
+ unless task_group.is_a?(TaskGroup)
49
+ raise ArgumentError.new("TaskGroup expected, got #{task_group.class}.")
50
+ end
51
+
52
+ if self[task_group.header]
53
+ raise ArgumentError.new("Task group with header #{task_group.header} is already on the list.")
54
+ end
55
+
56
+ @data << task_group
57
+ self.sort!
58
+ end
59
+
60
+ def scheduled_task_groups
61
+ @data.group_by { |tg| tg.scheduled_date.nil? }[false] || Array.new
62
+ end
63
+
64
+ def non_scheduled_task_groups
65
+ @data.group_by { |tg| tg.scheduled_date.nil? }[true] || Array.new
66
+ end
67
+
68
+ def sort!
69
+ @data = self.scheduled_task_groups.sort_by(&:scheduled_date) +
70
+ self.non_scheduled_task_groups
71
+
72
+ self
73
+ end
74
+
75
+ # Remove a task group from the task list.
76
+ #
77
+ # @param [TaskGroup] task_group the task group.
78
+ # @since 0.2
79
+ def delete(task_group)
80
+ @data.delete(task_group)
81
+ end
82
+
83
+ # Iterate over the task groups.
84
+ #
85
+ # @yieldparam [TaskGroup] task_group
86
+ # @since 0.2
87
+ def each(&block)
88
+ @data.each(&block)
89
+ end
90
+
91
+ # Return a scheduled task list formatted string.
92
+ #
93
+ # @since 0.2
94
+ def to_s
95
+ @data.map(&:to_s).join("\n")
96
+ end
97
+
98
+ def save(path)
99
+ data = self.to_s
100
+ File.open(path, 'w:utf-8') do |file|
101
+ file.puts(data)
102
+ end
103
+ end
104
+ end
105
+
106
+ export { TaskList }
metadata ADDED
@@ -0,0 +1,78 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: scheduled-format
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - James C Russell
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-06-07 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: commonjs_modules
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.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.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: parslet
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.8'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.8'
41
+ description: "."
42
+ email: james@101ideas.cz
43
+ executables: []
44
+ extensions: []
45
+ extra_rdoc_files: []
46
+ files:
47
+ - README.md
48
+ - lib/scheduled-format.rb
49
+ - lib/scheduled-format/parser/parser.rb
50
+ - lib/scheduled-format/parser/transformer.rb
51
+ - lib/scheduled-format/task.rb
52
+ - lib/scheduled-format/task_group.rb
53
+ - lib/scheduled-format/task_list.rb
54
+ homepage: http://github.com/botanicus/scheduled-format
55
+ licenses:
56
+ - MIT
57
+ metadata: {}
58
+ post_install_message:
59
+ rdoc_options: []
60
+ require_paths:
61
+ - lib
62
+ required_ruby_version: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: '0'
67
+ required_rubygems_version: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: '0'
72
+ requirements: []
73
+ rubyforge_project:
74
+ rubygems_version: 2.7.6
75
+ signing_key:
76
+ specification_version: 4
77
+ summary: ''
78
+ test_files: []