PageRenderTime 25ms CodeModel.GetById 31ms RepoModel.GetById 0ms app.codeStats 0ms

/TodoTxt/Task.php

https://github.com/rmasters/todo.txt-php
PHP | 306 lines | 143 code | 33 blank | 130 comment | 21 complexity | 7c7fd6273349af8fb68d9f85ac9de4db MD5 | raw file
  1. <?php
  2. namespace TodoTxt;
  3. /**
  4. * Encapsulates a single line of a todo.txt list.
  5. * Handles the parsing of contexts, projects and other info from a task.
  6. *
  7. * @TODO: Make the find* methods public static?
  8. * @TODO: Devise a good way to write plug-ins for this process. Possibly
  9. * by simply extending the class.
  10. * @TODO: Make a ContextList and ProjectList class to hold contexts and
  11. * projects (so we can do count($list->projects) etc.).
  12. * @TODO: Decide if returning silently is the best thing to do during
  13. * parsing, rather than throwing exceptions.
  14. */
  15. class Task
  16. {
  17. /* @var string The task as passed to the constructor. */
  18. protected $rawTask;
  19. /* @var string The task, sans priority, completion marker/date. */
  20. protected $task;
  21. /* @var boolean Whether the task has been completed. */
  22. protected $completed = false;
  23. /* @var DateTime The date the task was completed. */
  24. protected $completionDate;
  25. /* @var string A single-character, uppercase priority, if found. */
  26. protected $priority;
  27. /* @var DateTime The date the task was created. */
  28. protected $created;
  29. /* @var array A list of project names found (case-sensitive). */
  30. public $projects = array();
  31. /* @var array A list of context names found (case-sensitive). */
  32. public $contexts = array();
  33. /*
  34. * @var array A map of meta-data, contained in the task.
  35. * @see __get
  36. * @see __set
  37. */
  38. protected $metadata = array();
  39. /**
  40. * Create a new task from a raw line held in a todo.txt file.
  41. * @param string $task A raw task line
  42. * @throws EmptyString When $task is an empty string (or whitespace)
  43. */
  44. public function __construct($task) {
  45. $task = trim($task);
  46. if (strlen($task) == 0) {
  47. throw new Exception\EmptyString;
  48. }
  49. $this->rawTask = $task;
  50. // Since each of these parts can occur sequentially and only at
  51. // the start of the string, pass the remainder of the task on.
  52. $result = $this->findCompleted($task);
  53. $result = $this->findPriority($result);
  54. $result = $this->findCreated($result);
  55. /*$result = trim($result);
  56. if (strlen($result) == 0) {
  57. //throw new Exception\EmptyString;
  58. return null;
  59. }*/
  60. $this->task = $result;
  61. // Find metadata held in the rest of the task
  62. $this->findContexts($result);
  63. $this->findProjects($result);
  64. $this->findMetadata($result);
  65. }
  66. /**
  67. * Returns the age of the task if the task has a creation date.
  68. * @param DateTime|string $endDate The end-date to use if the task
  69. * does not have a completion date. If this is null and the task
  70. * doesn't have a completion date the current date will be used.
  71. * @return DateInterval The age of the task.
  72. * @throws CannotCalculateAge If the task does not have a creation date.
  73. */
  74. public function age($endDate = null) {
  75. if (!isset($this->created)) {
  76. throw new Exception\CannotCalculateAge;
  77. }
  78. // Decide on an end-date to use - completionDate, then a
  79. // provided date, then the current date.
  80. $end = new \DateTime("now");
  81. if (isset($this->completionDate)) {
  82. $end = $this->completionDate;
  83. } else if (!is_null($endDate)) {
  84. if (!($endDate instanceof \DateTime)) {
  85. $endDate = new \DateTime($endDate);
  86. }
  87. $end = $endDate;
  88. }
  89. $diff = $this->created->diff($end);
  90. if ($diff->invert) {
  91. throw new Exception\CompletionParadox;
  92. }
  93. return $diff;
  94. }
  95. /**
  96. * Add an array of projects to the list.
  97. * Using this method will prevent duplication in the array.
  98. * @param array $projects Array of project names.
  99. */
  100. public function addProjects(array $projects) {
  101. $projects = array_map("trim", $projects);
  102. $this->projects = array_unique(array_merge($this->projects, $projects));
  103. }
  104. /**
  105. * Add an array of contexts to the list.
  106. * Using this method will prevent duplication in the array.
  107. * @param array $contexts Array of context names.
  108. */
  109. public function addContexts(array $contexts) {
  110. $contexts = array_map("trim", $contexts);
  111. $this->contexts = array_unique(array_merge($this->contexts, $contexts));
  112. }
  113. /**
  114. * Access meta-properties, as held by key:value metadata in the task.
  115. * @param string $name The name of the meta-property.
  116. * @return string|null Value if property found, or null.
  117. */
  118. public function __get($name) {
  119. return isset($this->metadata[$name]) ? $this->metadata[$name] : null;
  120. }
  121. /**
  122. * Check for existence of a meta-property.
  123. * @param string $name The name of the meta-property.
  124. * @return boolean Whether the property is contained in the task.
  125. */
  126. public function __isset($name) {
  127. return isset($this->metadata[$name]);
  128. }
  129. /**
  130. * Re-build the task string.
  131. * @return string The task as a todo.txt line.
  132. */
  133. public function __toString() {
  134. $task = "";
  135. if ($this->completed) {
  136. $task .= sprintf("x %s ", $this->completionDate->format("Y-m-d"));
  137. }
  138. if (isset($this->priority)) {
  139. $task .= sprintf("(%s) ", strtoupper($this->priority));
  140. }
  141. if (isset($this->created)) {
  142. $task .= sprintf("%s ", $this->created->format("Y-m-d"));
  143. }
  144. $task .= $this->task;
  145. return $task;
  146. }
  147. public function isCompleted() {
  148. return $this->completed;
  149. }
  150. public function getCompletionDate() {
  151. return $this->isCompleted() && isset($this->completionDate) ? $this->completionDate : null;
  152. }
  153. public function getCreationDate() {
  154. return isset($this->created) ? $this->created : null;
  155. }
  156. /**
  157. * Get the remainder of the task (sans completed marker, creation
  158. * date and priority).
  159. */
  160. public function getTask() {
  161. return $this->task;
  162. }
  163. public function getPriority() {
  164. return $this->priority;
  165. }
  166. /**
  167. * Looks for a "x " marker, followed by a date.
  168. *
  169. * Complete tasks start with an X (case-insensitive), followed by a
  170. * space. The date of completion follows this (required).
  171. * Dates are formatted like YYYY-MM-DD.
  172. *
  173. * @param string $input String to check for completion.
  174. * @return string Returns the rest of the task, without this part.
  175. */
  176. protected function findCompleted($input) {
  177. // Match a lower or uppercase X, followed by a space and a
  178. // YYYY-MM-DD formatted date, followed by another space.
  179. // Invalid dates can be caught but checked after.
  180. $pattern = "/^(X|x) (\d{4}-\d{2}-\d{2}) /";
  181. if (preg_match($pattern, $input, $matches) == 1) {
  182. // Rather than throwing exceptions around, silently bypass this
  183. try {
  184. $this->completionDate = new \DateTime($matches[2]);
  185. } catch (\Exception $e) {
  186. return $input;
  187. }
  188. $this->completed = true;
  189. return substr($input, strlen($matches[0]));
  190. }
  191. return $input;
  192. }
  193. /**
  194. * Find a priority marker.
  195. * Priorities are signified by an uppercase letter in parentheses.
  196. *
  197. * @param string $input Input string to check.
  198. * @return string Returns the rest of the task, without this part.
  199. */
  200. protected function findPriority($input) {
  201. // Match one uppercase letter in brackers, followed by a space.
  202. $pattern = "/^\(([A-Z])\) /";
  203. if (preg_match($pattern, $input, $matches) == 1) {
  204. $this->priority = $matches[1];
  205. return substr($input, strlen($matches[0]));
  206. }
  207. return $input;
  208. }
  209. /**
  210. * Find a creation date (after a priority marker).
  211. * @param string $input Input string to check.
  212. * @return string Returns the rest of the task, without this part.
  213. */
  214. protected function findCreated($input) {
  215. // Match a YYYY-MM-DD formatted date, followed by a space.
  216. // Invalid dates can be caught but checked after.
  217. $pattern = "/^(\d{4}-\d{2}-\d{2}) /";
  218. if (preg_match($pattern, $input, $matches) == 1) {
  219. // Rather than throwing exceptions around, silently bypass this
  220. try {
  221. $this->created = new \DateTime($matches[1]);
  222. } catch (\Exception $e) {
  223. return $input;
  224. }
  225. return substr($input, strlen($matches[0]));
  226. }
  227. return $input;
  228. }
  229. /**
  230. * Find @contexts within the task
  231. * @param string $input Input string to check
  232. */
  233. protected function findContexts($input) {
  234. // Match an at-sign, any non-whitespace character, ending with
  235. // an alphanumeric or underscore, followed either by the end of
  236. // the string or by whitespace.
  237. $pattern = "/@(\S+\w)(?=\s|$)/";
  238. if (preg_match_all($pattern, $input, $matches) > 0) {
  239. $this->addContexts($matches[1]);
  240. }
  241. }
  242. /**
  243. * Find +projects within the task
  244. * @param string $input Input string to check
  245. */
  246. protected function findProjects($input) {
  247. // The same rules as contexts, except projects use a plus.
  248. $pattern = "/\+(\S+\w)(?=\s|$)/";
  249. if (preg_match_all($pattern, $input, $matches) > 0) {
  250. $this->addProjects($matches[1]);
  251. }
  252. }
  253. /**
  254. * Metadata can be held in the string in the format key:value.
  255. * This is usually used by add-ons, which provide their own
  256. * formatting rules for tasks.
  257. * This data can be accessed using __get() and __isset().
  258. *
  259. * @param string $input Input string to check
  260. * @see __get
  261. * @see __set
  262. */
  263. protected function findMetadata($input) {
  264. // Match a word (alphanumeric+underscores), a colon, followed by
  265. // any non-whitespace character.
  266. $pattern = "/(?<=\s|^)(\w+):(\S+)(?=\s|$)/";
  267. if (preg_match_all($pattern, $input, $matches, PREG_SET_ORDER) > 0) {
  268. foreach ($matches as $match) {
  269. $this->metadata[$match[1]] = $match[2];
  270. }
  271. }
  272. }
  273. }