/* * 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) { 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) { var inputText = task.title val tagPattern = Pattern.compile("(\\s|^)#(\\(.*\\)|[^\\s]+)") val contextPattern = Pattern.compile("(\\s|^)@(\\(.*\\)|[^\\s]+)") val addedTags: MutableSet = 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() 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 = 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() 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() // 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 = 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() 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 } }