// GetPlugin attempts to find a plugin and route for the given msg input if none // can be found, it checks the database for the last route used and gets the // plugin for that. If there is no previously used plugin, we return // errMissingPlugin. The bool value return indicates whether this plugin is // different from the last plugin used by the user. func GetPlugin(db *sqlx.DB, m *dt.Msg) (p *dt.Plugin, route string, directroute, followup bool, err error) { // Iterate through all intents to see if any plugin has been registered // for the route for _, i := range m.StructuredInput.Intents { route = "I_" + strings.ToLower(i) log.Debug("searching for route", route) if p = RegPlugins.Get(route); p != nil { // Found route. Return it return p, route, true, false, nil } } log.Debug("getting last plugin route") prevPlugin, prevRoute, err := m.GetLastPlugin(db) if err != nil && err != sql.ErrNoRows { return nil, "", false, false, err } if len(prevPlugin) > 0 { log.Debugf("found user's last plugin route: %s - %s\n", prevPlugin, prevRoute) } // Iterate over all command/object pairs and see if any plugin has been // registered for the resulting route eng := porter2.Stemmer for _, c := range m.StructuredInput.Commands { c = strings.ToLower(eng.Stem(c)) for _, o := range m.StructuredInput.Objects { o = strings.ToLower(eng.Stem(o)) route := "CO_" + c + "_" + o log.Debug("searching for route", route) if p = RegPlugins.Get(route); p != nil { // Found route. Return it followup := prevPlugin == p.Config.Name return p, route, true, followup, nil } } } // The user input didn't match any plugins. Let's see if the previous // route does if prevRoute != "" { if p = RegPlugins.Get(prevRoute); p != nil { // Prev route matches a pkg! Return it return p, prevRoute, false, true, nil } } // Sadly, if we've reached this point, we are at a loss. log.Debug("could not match user input to any plugin") return nil, "", false, false, errMissingPlugin }
// ParseFromTime parses a natural language string to determine most likely times // based on a set time "context." The time context changes the meaning of words // like "this Tuesday," "next Tuesday," etc. func ParseFromTime(t time.Time, nlTime string) []time.Time { r := strings.NewReplacer( ".", "", ",", "", "(", "", ")", "", "'", "", ) nlTime = r.Replace(nlTime) nlTime = strings.ToLower(nlTime) r = strings.NewReplacer( "at ", "", "time", "", "oclock", "", "am", "AM", "pm", "PM", " am", "AM", " pm", "PM", // 1st, 2nd, 3rd, 4th, 5th, etc. "1st", "1", "2nd", "2", "3rd", "3", "th", "", "21st", "21", "22nd", "22", "23rd", "23", "31st", "31", ) nlTime = r.Replace(nlTime) nlTime = strings.Title(nlTime) if nlTime == "Now" { return []time.Time{time.Now()} } st := strings.Fields(nlTime) transform := struct { Transform int Type timeTransform Multiplier int }{ Multiplier: 1, } stFull := "" var closeTime bool var idxRel int var loc *time.Location for i := range st { // Normalize days switch st[i] { case "Monday": st[i] = "Mon" transform.Type = transformDay case "Tuesday", "Tues": st[i] = "Tue" transform.Type = transformDay case "Wednesday": st[i] = "Wed" transform.Type = transformDay case "Thursday", "Thur", "Thurs": st[i] = "Thu" transform.Type = transformDay case "Friday": st[i] = "Fri" transform.Type = transformDay case "Saturday": st[i] = "Sat" transform.Type = transformDay case "Sunday": st[i] = "Sun" transform.Type = transformDay // Normalize months case "January": st[i] = "Jan" transform.Type = transformMonth case "February": st[i] = "Feb" transform.Type = transformMonth case "March": st[i] = "Mar" transform.Type = transformMonth case "April": st[i] = "Apr" transform.Type = transformMonth case "May": // No translation needed for May transform.Type = transformMonth case "June": st[i] = "Jun" transform.Type = transformMonth case "July": st[i] = "Jul" transform.Type = transformMonth case "August": st[i] = "Aug" transform.Type = transformMonth case "September", "Sept": st[i] = "Sep" transform.Type = transformMonth case "October": st[i] = "Oct" transform.Type = transformMonth case "November": st[i] = "Nov" transform.Type = transformMonth case "December": st[i] = "Dec" transform.Type = transformMonth // If non-deterministic timezone information is provided, // e.g. ET or Eastern rather than EST, then load the location. // Daylight Savings will be determined on parsing case "Pacific", "PT": st[i] = "" loc = loadLocation("America/Los_Angeles") case "Mountain", "MT": st[i] = "" loc = loadLocation("America/Denver") case "Central", "CT": st[i] = "" loc = loadLocation("America/Chicago") case "Eastern", "ET": st[i] = "" loc = loadLocation("America/New_York") // TODO Add the remaining timezones // Handle relative times. This currently does not handle // complex cases like "in 3 months and 2 days" case "Yesterday": st[i] = "" transform.Type = transformDay transform.Transform = 1 transform.Multiplier = -1 case "Tomorrow": st[i] = "" transform.Transform = 1 transform.Type = transformDay case "Today": st[i] = "" closeTime = true transform.Type = transformHour case "Ago", "Last": st[i] = "" transform.Transform = 1 transform.Multiplier *= -1 // e.g. "In an hour" case "Next", "From", "Now", "In": st[i] = "" transform.Transform = 1 case "Later": st[i] = "" transform.Transform = 6 case "Hour", "Hours": st[i] = "" idxRel = i closeTime = true transform.Type = transformHour case "Few", "Couple": st[i] = "" transform.Transform = 2 case "Min", "Mins", "Minute", "Minutes": st[i] = "" idxRel = i closeTime = true transform.Type = transformMinute case "Day", "Days": st[i] = "" idxRel = i transform.Type = transformDay case "Week", "Weeks": st[i] = "" idxRel = i transform.Type = transformDay transform.Multiplier = 7 case "Month", "Months": st[i] = "" idxRel = i transform.Type = transformMonth case "Year", "Years": st[i] = "" idxRel = i transform.Type = transformYear // Remove unnecessary but common expressions like "at", "time", // "oclock". case "At", "Time", "Oclock", "This", "The": st[i] = "" case "Noon": st[i] = "12PM" case "Supper", "Dinner": st[i] = "6PM" } if len(st[i]) > 0 { stFull += st[i] + " " } } normalized := strings.TrimRight(stFull, " ") var timeEmpty bool ts := []time.Time{} tme, err := parseTime(normalized) if err != nil { // Set the hour to 9am timeEmpty = true tme = time.Now().Round(time.Hour) val := 9 - tme.Hour() tme = tme.Add(time.Duration(val) * time.Hour) } if closeTime { tme = time.Now().Round(time.Minute) } ts = append(ts, tme) // TODO make more efficient. Handle in switch? tloc := timeLocation{loc: loc} ctx := &TimeContext{ampm: ampmNoTime, tz: tloc} if strings.Contains(normalized, "AM") { ctx.ampm = amTime } if strings.Contains(normalized, "UTC") { ctx.tz.utc = true } for _, ti := range ts { ctx = updateContext(ctx, ti, false) } // Ensure dates are reasonable even in the absence of information. // e.g. 2AM should parse to the current year, not 0000 ctx = completeContext(ctx, t) // Loop through a second time to apply the discovered context to each // time. Note that this doesn't support context switching, // e.g. "5PM CST or PST" or "5PM EST or 6PM PST", which is rare in // practice. Future versions may be adapted to support it. if ctx.ampm == ampmNoTime { halfLen := len(ts) // Double size of times for AM/PM ts = append(ts, ts...) for i := range ts { var hour int t := ts[i] if i < halfLen { hour = t.Hour() } else { hour = t.Hour() + 12 } ts[i] = time.Date(ctx.year, ctx.month, ctx.day, hour, t.Minute(), t.Second(), t.Nanosecond(), ctx.tz.loc) } } else { for i := range ts { t := ts[i] ts[i] = time.Date(ctx.year, ctx.month, ctx.day, t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), ctx.tz.loc) } } // If there's no relative transform, we're done. if transform.Type == transformInvalid { if timeEmpty { return []time.Time{} } if idxRel == 0 { return ts } } // Check our idxRel term for the word that preceeds it. If that's a // number, e.g. 2 days, then that number is our Transform. Note that // this doesn't handle fractional modifiers, like 2.5 days. if idxRel > 0 { val, err := strconv.Atoi(st[idxRel-1]) if err == nil { transform.Transform = val } } // Apply the transform log.Debugf("timeparse: normalized %q. %+v\n", normalized, transform) for i := range ts { switch transform.Type { case transformYear: ts[i] = ts[i].AddDate(transform.Transform*transform.Multiplier, 0, 0) case transformMonth: ts[i] = ts[i].AddDate(0, transform.Multiplier*transform.Transform, 0) case transformDay: ts[i] = ts[i].AddDate(0, 0, transform.Multiplier*transform.Transform) case transformHour: ts[i] = ts[i].Add(time.Duration(transform.Transform*transform.Multiplier) * time.Hour) case transformMinute: ts[i] = ts[i].Add(time.Duration(transform.Transform*transform.Multiplier) * time.Minute) } } log.Debug("timeparse: parsed times", ts) return ts }