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.
tasks/app/src/main/java/com/todoroo/astrid/utility/TitleParser.kt

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
}
}