scheduled-format 0.0.1

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