mirror of https://github.com/tasks/tasks
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
437 lines
19 KiB
Kotlin
437 lines
19 KiB
Kotlin
/*
|
|
* Copyright (c) 2012 Todoroo Inc
|
|
*
|
|
* See the file "LICENSE" for the full license governing this code.
|
|
*/
|
|
package com.todoroo.astrid.utility
|
|
|
|
import com.mdimension.jchronic.AstridChronic
|
|
import com.mdimension.jchronic.Chronic
|
|
import com.todoroo.astrid.data.Task
|
|
import com.todoroo.astrid.data.Task.Companion.createDueDate
|
|
import net.fortuna.ical4j.model.Recur.Frequency
|
|
import org.tasks.Strings.isNullOrEmpty
|
|
import org.tasks.data.TagDataDao
|
|
import org.tasks.repeats.RecurrenceUtils.newRecur
|
|
import timber.log.Timber
|
|
import java.util.Calendar
|
|
import java.util.Locale
|
|
import java.util.regex.Matcher
|
|
import java.util.regex.Pattern
|
|
|
|
object TitleParser {
|
|
suspend fun parse(tagDataDao: TagDataDao, task: Task, tags: ArrayList<String>) {
|
|
repeatHelper(task)
|
|
listHelper(
|
|
tagDataDao,
|
|
task,
|
|
tags) // Don't need to know if tags affected things since we don't show alerts for them
|
|
dayHelper(task)
|
|
priorityHelper(task)
|
|
}
|
|
|
|
fun trimParenthesis(pattern: String): String {
|
|
var pattern = pattern
|
|
if (pattern[0] == '#' || pattern[0] == '@') {
|
|
pattern = pattern.substring(1)
|
|
}
|
|
return if ('(' == pattern[0]) {
|
|
pattern.substring(1, pattern.length - 1)
|
|
} else pattern
|
|
}
|
|
|
|
suspend fun listHelper(tagDataDao: TagDataDao, task: Task, tags: ArrayList<String>) {
|
|
var inputText = task.title
|
|
val tagPattern = Pattern.compile("(\\s|^)#(\\(.*\\)|[^\\s]+)")
|
|
val contextPattern = Pattern.compile("(\\s|^)@(\\(.*\\)|[^\\s]+)")
|
|
val addedTags: MutableSet<String?> = HashSet()
|
|
while (true) {
|
|
var m = tagPattern.matcher(inputText)
|
|
if (m.find()) {
|
|
val tag = trimParenthesis(m.group(2))
|
|
tagDataDao
|
|
.getTagWithCase(tag)
|
|
?.let {
|
|
if (!addedTags.contains(it)) {
|
|
tags.add(it)
|
|
}
|
|
addedTags.add(it)
|
|
}
|
|
} else {
|
|
m = contextPattern.matcher(inputText)
|
|
if (m.find()) {
|
|
val tag = trimParenthesis(m.group(2))
|
|
tagDataDao
|
|
.getTagWithCase(tag)
|
|
?.let {
|
|
if (!addedTags.contains(it)) {
|
|
tags.add(it)
|
|
}
|
|
addedTags.add(it)
|
|
}
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
inputText = inputText!!.substring(0, m.start()) + inputText.substring(m.end())
|
|
}
|
|
task.title = inputText!!.trim { it <= ' ' }
|
|
}
|
|
|
|
private fun strToPriority(priorityStr: String?): Int {
|
|
priorityStr?.lowercase(Locale.getDefault())?.trim { it <= ' ' }
|
|
var priority = Task.Priority.HIGH
|
|
if ("0" == priorityStr || "!0" == priorityStr || "least" == priorityStr || "lowest" == priorityStr) {
|
|
priority = Task.Priority.NONE
|
|
}
|
|
if ("!" == priorityStr || "!1" == priorityStr || "bang" == priorityStr || "1" == priorityStr || "low" == priorityStr) {
|
|
priority = Task.Priority.LOW
|
|
}
|
|
if ("!!" == priorityStr || "!2" == priorityStr || "bang bang" == priorityStr || "2" == priorityStr || "high" == priorityStr) {
|
|
priority = Task.Priority.MEDIUM
|
|
}
|
|
return priority
|
|
}
|
|
|
|
// priorityHelper parses the string and sets the Task's importance
|
|
private fun priorityHelper(task: Task) {
|
|
var inputText = task.title
|
|
val importanceStrings = arrayOf(
|
|
"""()((^|[^\w!])!+|(^|[^\w!])!\d)($|[^\w!])""",
|
|
"""()(?i)((\s?bang){1,})$""",
|
|
"""(?i)(\spriority\s?(\d)$)""",
|
|
"""(?i)(\sbang\s?(\d)$)""",
|
|
"""(?i)()(\shigh(est)?|\slow(est)?|\stop|\sleast) ?priority$"""
|
|
)
|
|
for (importanceString in importanceStrings) {
|
|
val importancePattern = Pattern.compile(importanceString)
|
|
while (true) {
|
|
val m = importancePattern.matcher(inputText)
|
|
if (m.find()) {
|
|
task.priority = strToPriority(m.group(2).trim { it <= ' ' })
|
|
val start = if (m.start() == 0) 0 else m.start() + 1
|
|
inputText = inputText!!.substring(0, start) + inputText.substring(m.end())
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
task.title = inputText!!.trim { it <= ' ' }
|
|
}
|
|
|
|
// helper for dayHelper. Converts am/pm to an int 0/1.
|
|
private fun ampmToNumber(amPmString: String?): Int {
|
|
var time = Calendar.PM
|
|
if (amPmString == null) {
|
|
return time
|
|
}
|
|
val text = amPmString.lowercase(Locale.getDefault()).trim { it <= ' ' }
|
|
if (text == "am" || text == "a.m" || text == "a") {
|
|
time = Calendar.AM
|
|
}
|
|
if (text == "pm" || text == "p.m" || text == "p") {
|
|
time = Calendar.PM
|
|
}
|
|
return time
|
|
}
|
|
|
|
private fun removeIfParenthetical(m: Matcher, inputText: String?): String? {
|
|
val s = m.group()
|
|
return if (s.startsWith("(") && s.endsWith(")")) {
|
|
inputText!!.substring(0, m.start()) + inputText.substring(m.end())
|
|
} else inputText
|
|
}
|
|
|
|
private fun stripParens(s: String): String {
|
|
var s = s
|
|
if (s.startsWith("(")) {
|
|
s = s.substring(1)
|
|
}
|
|
if (s.endsWith(")")) {
|
|
s = s.substring(0, s.length - 1)
|
|
}
|
|
return s
|
|
}
|
|
|
|
// ---------------------DATE--------------------------
|
|
// Handles setting the task's date.
|
|
// Day of week (e.g. Monday, Tuesday,..) is overridden by a set date (e.g. October 23 2013).
|
|
// Vague times (e.g. breakfast, night) are overridden by a set time (9 am, at 10, 17:00)
|
|
private fun dayHelper(task: Task) {
|
|
var inputText = task.title
|
|
var cal: Calendar? = null
|
|
var containsSpecificTime = false
|
|
val daysOfWeek = arrayOf(
|
|
"(?i)(\\(|\\b)today(\\)|\\b)",
|
|
"(?i)(\\(|\\b)tomorrow(\\)|\\b)",
|
|
"(?i)(\\(|\\b)mon(day(\\)|\\b)|(\\)|\\.))",
|
|
"(?i)(\\(|\\b)tue(sday(\\)|\\b)|(\\)|\\.))",
|
|
"(?i)(\\(|\\b)wed(nesday(\\)|\\b)|(\\)|\\.))",
|
|
"(?i)(\\(|\\b)thu(rsday(\\)|\\b)|(\\)|\\.))",
|
|
"(?i)(\\(|\\b)fri(day(\\)|\\b)|(\\)|\\.))",
|
|
"(?i)(\\(|\\b)sat(urday(\\)|\\b)|(\\)|\\.))",
|
|
"(?i)(\\(|\\b)sun(day(\\)|\\b)|(\\)|\\.))"
|
|
)
|
|
for (date in daysOfWeek) {
|
|
val pattern = Pattern.compile(date)
|
|
val m = pattern.matcher(inputText)
|
|
if (m.find()) {
|
|
val toParse = stripParens(m.group(0))
|
|
cal = AstridChronic.parse(toParse).beginCalendar
|
|
inputText = removeIfParenthetical(m, inputText)
|
|
// then put it into task
|
|
}
|
|
}
|
|
val dates = arrayOf(
|
|
"(?i)(\\(|\\b)(jan(\\.|uary))(\\s(3[0-1]|[0-2]?[0-9])),?( (\\d{4}|\\d{2}))?(\\)|\\b)",
|
|
"(?i)(\\(|\\b)(feb(\\.|ruary))(\\s(3[0-1]|[0-2]?[0-9])),?( (\\d{4}|\\d{2}))?(\\)|\\b)",
|
|
"(?i)(\\(|\\b)(mar(\\.|ch))(\\s(3[0-1]|[0-2]?[0-9])),?( (\\d{4}|\\d{2}))?(\\)|\\b)",
|
|
"(?i)(\\(|\\b)(apr(\\.|il))(\\s(3[0-1]|[0-2]?[0-9])),?( (\\d{4}|\\d{2}))?(\\)|\\b)",
|
|
"(?i)(\\(|\\b)(may())(\\s(3[0-1]|[0-2]?[0-9])),?( (\\d{4}|\\d{2}))?(\\)|\\b)",
|
|
"(?i)(\\(|\\b)(jun(\\.|e))(\\s(3[0-1]|[0-2]?[0-9])),?( (\\d{4}|\\d{2}))?(\\)|\\b)",
|
|
"(?i)(\\(|\\b)(jul(\\.|y))(\\s(3[0-1]|[0-2]?[0-9])),?( (\\d{4}|\\d{2}))?(\\)|\\b)",
|
|
"(?i)(\\(|\\b)(aug(\\.|ust))(\\s(3[0-1]|[0-2]?[0-9])),?( (\\d{4}|\\d{2}))?(\\)|\\b)",
|
|
"(?i)(\\(|\\b)(sep(\\.|tember))(\\s(3[0-1]|[0-2]?[0-9])),?( (\\d{4}|\\d{2}))?(\\)|\\b)",
|
|
"(?i)(\\(|\\b)(oct(\\.|ober))(\\s(3[0-1]|[0-2]?[0-9])),?( (\\d{4}|\\d{2}))?(\\)|\\b)",
|
|
"(?i)(\\(|\\b)(nov(\\.|ember))(\\s(3[0-1]|[0-2]?[0-9])),?( (\\d{4}|\\d{2}))?(\\)|\\b)",
|
|
"(?i)(\\(|\\b)(dec(\\.|ember))(\\s(3[0-1]|[0-2]?[0-9])),?( (\\d{4}|\\d{2}))?(\\)|\\b)"
|
|
)
|
|
|
|
// m.group(2) = "month"
|
|
// m.group(5) = "day"
|
|
for (date in dates) {
|
|
val pattern = Pattern.compile(date)
|
|
val m = pattern.matcher(inputText)
|
|
if (m.find()) {
|
|
val dateCal = Chronic.parse(m.group(2)).beginCalendar
|
|
if (m.group(5) != null) {
|
|
dateCal[Calendar.DAY_OF_MONTH] = m.group(5).toInt()
|
|
}
|
|
val today = Calendar.getInstance()
|
|
if (m.group(6) != null) {
|
|
dateCal[Calendar.YEAR] = m.group(6).trim { it <= ' ' }.toInt()
|
|
} else if (today[Calendar.MONTH] - dateCal[Calendar.MONTH]
|
|
> 1) { // if more than a month in the past
|
|
dateCal[Calendar.YEAR] = dateCal[Calendar.YEAR] + 1
|
|
}
|
|
if (cal == null) {
|
|
cal = dateCal
|
|
} else {
|
|
cal[Calendar.DAY_OF_MONTH] = dateCal[Calendar.DAY_OF_MONTH]
|
|
cal[Calendar.MONTH] = dateCal[Calendar.MONTH]
|
|
cal[Calendar.YEAR] = dateCal[Calendar.YEAR]
|
|
}
|
|
inputText = removeIfParenthetical(m, inputText)
|
|
}
|
|
}
|
|
|
|
// for dates in the format MM/DD
|
|
val p = Pattern.compile(
|
|
"(?i)(\\(|\\b)(1[0-2]|0?[1-9])(\\/|-)(3[0-1]|[0-2]?[0-9])(\\/|-)?(\\d{4}|\\d{2})?(\\)|\\b)")
|
|
val match = p.matcher(inputText)
|
|
if (match.find()) {
|
|
val dCal = Calendar.getInstance()
|
|
setCalendarToDefaultTime(dCal)
|
|
dCal[Calendar.MONTH] = match.group(2).trim { it <= ' ' }.toInt() - 1
|
|
dCal[Calendar.DAY_OF_MONTH] = match.group(4).toInt()
|
|
if (match.group(6) != null && match.group(6).trim { it <= ' ' } != "") {
|
|
var yearString = match.group(6)
|
|
if (match.group(6).length == 2) {
|
|
yearString = "20" + match.group(6)
|
|
}
|
|
dCal[Calendar.YEAR] = yearString.toInt()
|
|
}
|
|
if (cal == null) {
|
|
cal = dCal
|
|
} else {
|
|
cal[Calendar.DAY_OF_MONTH] = dCal[Calendar.DAY_OF_MONTH]
|
|
cal[Calendar.MONTH] = dCal[Calendar.MONTH]
|
|
cal[Calendar.YEAR] = dCal[Calendar.YEAR]
|
|
}
|
|
inputText = removeIfParenthetical(match, inputText)
|
|
}
|
|
val dayTimes = HashMap<String, Int>()
|
|
dayTimes["(?i)\\bbreakfast\\b"] = 8
|
|
dayTimes["(?i)\\blunch\\b"] = 12
|
|
dayTimes["(?i)\\bsupper\\b"] = 18
|
|
dayTimes["(?i)\\bdinner\\b"] = 18
|
|
dayTimes["(?i)\\bbrunch\\b"] = 10
|
|
dayTimes["(?i)\\bmorning\\b"] = 8
|
|
dayTimes["(?i)\\bafternoon\\b"] = 15
|
|
dayTimes["(?i)\\bevening\\b"] = 19
|
|
dayTimes["(?i)\\bnight\\b"] = 19
|
|
dayTimes["(?i)\\bmidnight\\b"] = 0
|
|
dayTimes["(?i)\\bnoon\\b"] = 12
|
|
val keys: Set<String> = dayTimes.keys
|
|
for (dayTime in keys) {
|
|
val pattern = Pattern.compile(dayTime)
|
|
val m = pattern.matcher(inputText)
|
|
if (m.find()) {
|
|
containsSpecificTime = true
|
|
val timeHour = dayTimes[dayTime]!!
|
|
val dayTimesCal = Calendar.getInstance()
|
|
setCalendarToDefaultTime(dayTimesCal)
|
|
dayTimesCal[Calendar.HOUR] = timeHour
|
|
if (cal == null) {
|
|
cal = dayTimesCal
|
|
} else {
|
|
setCalendarToDefaultTime(cal)
|
|
cal[Calendar.HOUR] = timeHour
|
|
}
|
|
}
|
|
}
|
|
val times = arrayOf( // [time] am/pm
|
|
"(?i)(\\b)([01]?\\d):?([0-5]\\d)? ?([ap]\\.?m?\\.?)\\b", // army time
|
|
"(?i)\\b(([0-2]?[0-9]):([0-5][0-9]))(\\b)", // [int] o'clock
|
|
"(?i)\\b(([01]?\\d)() ?o'? ?clock) ?([ap]\\.?m\\.?)?\\b", // at [int]
|
|
"(?i)(\\bat) ([01]?\\d)()($|\\D($|\\D))" // m.group(2) holds the hour
|
|
// m.group(3) holds the minutes
|
|
// m.group(4) holds am/pm
|
|
)
|
|
for (time in times) {
|
|
val pattern = Pattern.compile(time)
|
|
val m = pattern.matcher(inputText)
|
|
if (m.find()) {
|
|
containsSpecificTime = true
|
|
val today = Calendar.getInstance()
|
|
val timeCal = Calendar.getInstance()
|
|
setCalendarToDefaultTime(timeCal)
|
|
timeCal[Calendar.HOUR] = m.group(2).toInt()
|
|
if (m.group(3) != null && m.group(3).trim { it <= ' ' } != "") {
|
|
timeCal[Calendar.MINUTE] = m.group(3).toInt()
|
|
} else {
|
|
timeCal[Calendar.MINUTE] = 0
|
|
}
|
|
if (m.group(2).toInt() <= 12) {
|
|
timeCal[Calendar.AM_PM] = ampmToNumber(m.group(4))
|
|
}
|
|
|
|
// sets it to the next occurrence of that hour if no am/pm is provided. doesn't include
|
|
// military time
|
|
if (m.group(2).toInt() <= 12
|
|
&& (m.group(4) == null || m.group(4).trim { it <= ' ' } == "")) {
|
|
while (timeCal.time.time < today.time.time) {
|
|
timeCal[Calendar.HOUR_OF_DAY] = timeCal[Calendar.HOUR_OF_DAY] + 12
|
|
}
|
|
} else { // if am/pm is provided and the time is in the past, set it to the next day.
|
|
// Military time included.
|
|
if (timeCal[Calendar.HOUR] != 0
|
|
&& timeCal.time.time < today.time.time) {
|
|
timeCal[Calendar.DAY_OF_MONTH] = timeCal[Calendar.DAY_OF_MONTH] + 1
|
|
}
|
|
if (timeCal[Calendar.HOUR] == 0) {
|
|
timeCal[Calendar.HOUR] = 12
|
|
}
|
|
}
|
|
if (cal == null) {
|
|
cal = timeCal
|
|
} else {
|
|
cal[Calendar.HOUR] = timeCal[Calendar.HOUR]
|
|
cal[Calendar.MINUTE] = timeCal[Calendar.MINUTE]
|
|
cal[Calendar.SECOND] = timeCal[Calendar.SECOND]
|
|
cal[Calendar.AM_PM] = timeCal[Calendar.AM_PM]
|
|
}
|
|
break
|
|
}
|
|
}
|
|
if (cal
|
|
!= null) { // if at least one of the above has been called, write to task. else do nothing.
|
|
if (!isNullOrEmpty(inputText)) {
|
|
task.title = inputText
|
|
}
|
|
if (containsSpecificTime) {
|
|
task.dueDate = createDueDate(Task.URGENCY_SPECIFIC_DAY_TIME, cal.time.time)
|
|
} else {
|
|
task.dueDate = createDueDate(Task.URGENCY_SPECIFIC_DAY, cal.time.time)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---------------------DATE--------------------------
|
|
// Parses through the text and sets the frequency of the task.
|
|
private fun repeatHelper(task: Task) {
|
|
val inputText = task.title
|
|
val repeatTimes = HashMap<String, Frequency>()
|
|
repeatTimes["(?i)\\bevery ?\\w{0,6} days?\\b"] = Frequency.DAILY
|
|
repeatTimes["(?i)\\bevery ?\\w{0,6} ?nights?\\b"] = Frequency.DAILY
|
|
repeatTimes["(?i)\\bevery ?\\w{0,6} ?mornings?\\b"] = Frequency.DAILY
|
|
repeatTimes["(?i)\\bevery ?\\w{0,6} ?evenings?\\b"] = Frequency.DAILY
|
|
repeatTimes["(?i)\\bevery ?\\w{0,6} ?afternoons?\\b"] = Frequency.DAILY
|
|
repeatTimes["(?i)\\bevery \\w{0,6} ?weeks?\\b"] = Frequency.WEEKLY
|
|
repeatTimes["(?i)\\bevery \\w{0,6} ?(mon|tues|wednes|thurs|fri|satur|sun)days?\\b"] = Frequency.WEEKLY
|
|
repeatTimes["(?i)\\bevery \\w{0,6} ?months?\\b"] = Frequency.MONTHLY
|
|
repeatTimes["(?i)\\bevery \\w{0,6} ?years?\\b"] = Frequency.YEARLY
|
|
val repeatTimesIntervalOne = HashMap<String, Frequency>()
|
|
// pre-determined intervals of 1
|
|
repeatTimesIntervalOne["(?i)\\bdaily\\b"] = Frequency.DAILY
|
|
repeatTimesIntervalOne["(?i)\\beveryday\\b"] = Frequency.DAILY
|
|
repeatTimesIntervalOne["(?i)\\bweekly\\b"] = Frequency.WEEKLY
|
|
repeatTimesIntervalOne["(?i)\\bmonthly\\b"] = Frequency.MONTHLY
|
|
repeatTimesIntervalOne["(?i)\\byearly\\b"] = Frequency.YEARLY
|
|
val keys: Set<String> = repeatTimes.keys
|
|
for (repeatTime in keys) {
|
|
val pattern = Pattern.compile(repeatTime)
|
|
val m = pattern.matcher(inputText)
|
|
if (m.find()) {
|
|
val rtime = repeatTimes[repeatTime]
|
|
val recur = newRecur()
|
|
recur.setFrequency(rtime!!.name)
|
|
recur.interval = findInterval(inputText)
|
|
task.recurrence = recur.toString()
|
|
return
|
|
}
|
|
}
|
|
for (repeatTimeIntervalOne in repeatTimesIntervalOne.keys) {
|
|
val pattern = Pattern.compile(repeatTimeIntervalOne)
|
|
val m = pattern.matcher(inputText)
|
|
if (m.find()) {
|
|
val rtime = repeatTimesIntervalOne[repeatTimeIntervalOne]
|
|
val recur = newRecur()
|
|
recur.setFrequency(rtime!!.name)
|
|
recur.interval = 1
|
|
task.recurrence = recur.toString()
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// helper method for repeatHelper.
|
|
private fun findInterval(inputText: String?): Int {
|
|
val wordsToNum = HashMap<String, Int?>()
|
|
val words = arrayOf(
|
|
"one", "two", "three", "four", "five", "six",
|
|
"seven", "eight", "nine", "ten", "eleven", "twelve"
|
|
)
|
|
for (i in words.indices) {
|
|
wordsToNum[words[i]] = i + 1
|
|
wordsToNum[(i + 1).toString()] = i + 1
|
|
}
|
|
wordsToNum["other"] = 2
|
|
val pattern = Pattern.compile("(?i)\\bevery (\\w*)\\b")
|
|
var interval = 1
|
|
val m = pattern.matcher(inputText)
|
|
if (m.find() && m.group(1) != null) {
|
|
val intervalStr = m.group(1)
|
|
if (wordsToNum.containsKey(intervalStr)) {
|
|
interval = wordsToNum[intervalStr]!!
|
|
} else {
|
|
try {
|
|
interval = intervalStr.toInt()
|
|
} catch (e: NumberFormatException) {
|
|
// Ah well
|
|
Timber.e(e)
|
|
}
|
|
}
|
|
}
|
|
return interval
|
|
}
|
|
|
|
// helper method for DayHelper. Resets the time on the calendar to 00:00:00 am
|
|
private fun setCalendarToDefaultTime(cal: Calendar) {
|
|
cal[Calendar.HOUR] = 0
|
|
cal[Calendar.HOUR_OF_DAY] = 0
|
|
cal[Calendar.MINUTE] = 0
|
|
cal[Calendar.SECOND] = 0
|
|
cal[Calendar.AM_PM] = Calendar.AM
|
|
}
|
|
} |