// Parse "last minute", "next hour"
        private DateTimeResolutionResult ParseRelativeUnit(string text, DateObject referenceTime)
        {
            var ret = new DateTimeResolutionResult();

            var match = Config.RelativeTimeUnitRegex.Match(text);

            if (!match.Success)
            {
                match = this.Config.RestOfDateTimeRegex.Match(text);
            }

            if (match.Success)
            {
                var srcUnit = match.Groups["unit"].Value.ToLower();

                var unitStr = Config.UnitMap[srcUnit];

                int swiftValue  = 1;
                var prefixMatch = Config.PastRegex.Match(text);
                if (prefixMatch.Success)
                {
                    swiftValue = -1;
                }

                DateObject beginTime;
                var        endTime = beginTime = referenceTime;
                var        ptTimex = string.Empty;

                if (Config.UnitMap.ContainsKey(srcUnit))
                {
                    switch (unitStr)
                    {
                    case "D":
                        endTime = DateObject.MinValue.SafeCreateFromValue(beginTime.Year, beginTime.Month, beginTime.Day);
                        endTime = endTime.AddDays(1).AddSeconds(-1);
                        ptTimex = "PT" + (endTime - beginTime).TotalSeconds + "S";
                        break;

                    case "H":
                        beginTime = swiftValue > 0 ? beginTime : referenceTime.AddHours(swiftValue);
                        endTime   = swiftValue > 0 ? referenceTime.AddHours(swiftValue) : endTime;
                        ptTimex   = "PT1H";
                        break;

                    case "M":
                        beginTime = swiftValue > 0 ? beginTime : referenceTime.AddMinutes(swiftValue);
                        endTime   = swiftValue > 0 ? referenceTime.AddMinutes(swiftValue) : endTime;
                        ptTimex   = "PT1M";
                        break;

                    case "S":
                        beginTime = swiftValue > 0 ? beginTime : referenceTime.AddSeconds(swiftValue);
                        endTime   = swiftValue > 0 ? referenceTime.AddSeconds(swiftValue) : endTime;
                        ptTimex   = "PT1S";
                        break;

                    default:
                        return(ret);
                    }

                    ret.Timex =
                        $"({FormatUtil.LuisDate(beginTime)}T{FormatUtil.LuisTime(beginTime)}," +
                        $"{FormatUtil.LuisDate(endTime)}T{FormatUtil.LuisTime(endTime)},{ptTimex})";

                    ret.FutureValue = ret.PastValue = new Tuple <DateObject, DateObject>(beginTime, endTime);
                    ret.Success     = true;

                    return(ret);
                }
            }

            return(ret);
        }
        //TODO: this can be abstracted with the similar method in BaseDatePeriodParser
        // parse "in 20 minutes"
        private DateTimeResolutionResult ParseDuration(string text, DateObject referenceTime)
        {
            var ret = new DateTimeResolutionResult();

            //for rest of datetime, it will be handled in next function
            if (Config.RestOfDateTimeRegex.IsMatch(text))
            {
                return(ret);
            }

            var ers = Config.DurationExtractor.Extract(text);

            if (ers.Count == 1)
            {
                var pr        = Config.DurationParser.Parse(ers[0]);
                var beforeStr = text.Substring(0, pr.Start ?? 0).Trim().ToLowerInvariant();
                if (pr.Value != null)
                {
                    var swiftSeconds   = 0;
                    var mod            = "";
                    var durationResult = (DateTimeResolutionResult)pr.Value;
                    if (durationResult.PastValue is double && durationResult.FutureValue is double)
                    {
                        swiftSeconds = (int)((double)durationResult.FutureValue);
                    }

                    DateObject beginTime;
                    var        endTime = beginTime = referenceTime;

                    var prefixMatch = Config.PastRegex.Match(beforeStr);
                    if (prefixMatch.Success && prefixMatch.Length == beforeStr.Length)
                    {
                        mod       = TimeTypeConstants.beforeMod;
                        beginTime = referenceTime.AddSeconds(-swiftSeconds);
                    }

                    prefixMatch = Config.FutureRegex.Match(beforeStr);
                    if (prefixMatch.Success && prefixMatch.Length == beforeStr.Length)
                    {
                        mod     = TimeTypeConstants.afterMod;
                        endTime = beginTime.AddSeconds(swiftSeconds);
                    }

                    ret.Timex =
                        $"({FormatUtil.LuisDate(beginTime)}T{FormatUtil.LuisTime(beginTime)}," +
                        $"{FormatUtil.LuisDate(endTime)}T{FormatUtil.LuisTime(endTime)}," +
                        $"{durationResult.Timex})";
                    ret.FutureValue = ret.PastValue = new Tuple <DateObject, DateObject>(beginTime, endTime);
                    ret.Success     = true;

                    if (!string.IsNullOrEmpty(mod))
                    {
                        ((DateTimeResolutionResult)pr.Value).Mod = mod;
                    }
                    ret.SubDateTimeEntities = new List <object> {
                        pr
                    };

                    return(ret);
                }
            }

            return(ret);
        }
        //TODO: this can be abstracted with the similar method in BaseDatePeriodParser
        // Parse "in 20 minutes"
        private DateTimeResolutionResult ParseDuration(string text, DateObject referenceTime)
        {
            var ret = new DateTimeResolutionResult();

            // For the rest of datetime, it will be handled in next function
            if (Config.RestOfDateTimeRegex.IsMatch(text))
            {
                return(ret);
            }

            var ers = Config.DurationExtractor.Extract(text, referenceTime);

            if (ers.Count == 1)
            {
                var pr = Config.DurationParser.Parse(ers[0]);

                var beforeStr = text.Substring(0, pr.Start ?? 0).Trim().ToLowerInvariant();
                var afterStr  = text.Substring((pr.Start ?? 0) + (pr.Length ?? 0)).Trim().ToLowerInvariant();

                if (pr.Value != null)
                {
                    var swiftSeconds   = 0;
                    var mod            = "";
                    var durationResult = (DateTimeResolutionResult)pr.Value;
                    if (durationResult.PastValue is double && durationResult.FutureValue is double)
                    {
                        swiftSeconds = (int)((double)durationResult.FutureValue);
                    }

                    DateObject beginTime;
                    var        endTime = beginTime = referenceTime;

                    var prefixMatch = Config.PastRegex.Match(beforeStr);
                    if (prefixMatch.Success && prefixMatch.Length == beforeStr.Length)
                    {
                        mod       = Constants.BEFORE_MOD;
                        beginTime = referenceTime.AddSeconds(-swiftSeconds);
                    }

                    // Handle the "within (the) (next) xx seconds/minutes/hours" case
                    // Should also handle the multiple duration case like P1DT8H
                    // Set the beginTime equal to reference time for now
                    prefixMatch = Config.WithinNextPrefixRegex.Match(beforeStr);
                    if (prefixMatch.Success && prefixMatch.Length == beforeStr.Length)
                    {
                        endTime = beginTime.AddSeconds(swiftSeconds);
                    }

                    prefixMatch = Config.FutureRegex.Match(beforeStr);
                    if (prefixMatch.Success && prefixMatch.Length == beforeStr.Length)
                    {
                        mod     = Constants.AFTER_MOD;
                        endTime = beginTime.AddSeconds(swiftSeconds);
                    }

                    var suffixMatch = Config.PastRegex.Match(afterStr);
                    if (suffixMatch.Success && suffixMatch.Length == afterStr.Length)
                    {
                        mod       = Constants.BEFORE_MOD;
                        beginTime = referenceTime.AddSeconds(-swiftSeconds);
                    }

                    suffixMatch = Config.FutureRegex.Match(afterStr);
                    if (suffixMatch.Success && suffixMatch.Length == afterStr.Length)
                    {
                        mod     = Constants.AFTER_MOD;
                        endTime = beginTime.AddSeconds(swiftSeconds);
                    }

                    suffixMatch = Config.FutureSuffixRegex.Match(afterStr);
                    if (suffixMatch.Success && suffixMatch.Length == afterStr.Length)
                    {
                        mod     = Constants.AFTER_MOD;
                        endTime = beginTime.AddSeconds(swiftSeconds);
                    }

                    ret.Timex =
                        $"({FormatUtil.LuisDate(beginTime)}T{FormatUtil.LuisTime(beginTime)}," +
                        $"{FormatUtil.LuisDate(endTime)}T{FormatUtil.LuisTime(endTime)}," +
                        $"{durationResult.Timex})";

                    ret.FutureValue = ret.PastValue = new Tuple <DateObject, DateObject>(beginTime, endTime);
                    ret.Success     = true;

                    if (!string.IsNullOrEmpty(mod))
                    {
                        ((DateTimeResolutionResult)pr.Value).Mod = mod;
                    }

                    ret.SubDateTimeEntities = new List <object> {
                        pr
                    };

                    return(ret);
                }
            }

            return(ret);
        }
        public SortedDictionary <string, object> DateTimeResolution(DateTimeParseResult slot)
        {
            if (slot == null)
            {
                return(null);
            }

            var resolutions = new List <Dictionary <string, object> >();
            var res         = new Dictionary <string, object>();

            var type  = slot.Type;
            var timex = slot.TimexStr;

            var val = (DateTimeResolutionResult)slot.Value;

            if (val == null)
            {
                return(null);
            }

            var           islunar = val.IsLunar;
            var           mod     = val.Mod;
            List <string> list    = null;

            // Resolve dates list for date periods
            if (slot.Type.Equals(Constants.SYS_DATETIME_DATEPERIOD) && val.List != null)
            {
                list = val.List.Select(o => FormatUtil.LuisDate((DateObject)o)).ToList();
            }

            // With modifier, output Type might not be the same with type in resolution result
            // For example, if the resolution type is "date", with modifier the output type should be "daterange"
            var typeOutput = DetermineDateTimeType(slot.Type, hasMod: !string.IsNullOrEmpty(mod));
            var comment    = val.Comment;

            // The following should be added to res first, since ResolveAmPm requires these fields.
            AddResolutionFields(res, DateTimeResolutionKey.Timex, timex);
            AddResolutionFields(res, Constants.Comment, comment);
            AddResolutionFields(res, DateTimeResolutionKey.Mod, mod);
            AddResolutionFields(res, ResolutionKey.Type, typeOutput);
            AddResolutionFields(res, DateTimeResolutionKey.IsLunar, islunar ? islunar.ToString() : string.Empty);

            var hasTimeZone = false;

            // For standalone timezone entity recognition, we generate TimeZoneResolution for each entity we extracted.
            // We also merge time entity with timezone entity and add the information in TimeZoneResolution to every DateTime resolutions.
            if (val.TimeZoneResolution != null)
            {
                if (slot.Type.Equals(Constants.SYS_DATETIME_TIMEZONE))
                {
                    // single timezone
                    AddResolutionFields(res, Constants.ResolveTimeZone, new Dictionary <string, string>
                    {
                        { ResolutionKey.Value, val.TimeZoneResolution.Value },
                        { Constants.UtcOffsetMinsKey, val.TimeZoneResolution.UtcOffsetMins.ToString() }
                    });
                }
                else
                {
                    // timezone as clarification of datetime
                    hasTimeZone = true;
                    AddResolutionFields(res, Constants.TimeZone, val.TimeZoneResolution.Value);
                    AddResolutionFields(res, Constants.TimeZoneText, val.TimeZoneResolution.TimeZoneText);
                    AddResolutionFields(res, Constants.UtcOffsetMinsKey, val.TimeZoneResolution.UtcOffsetMins.ToString());
                }
            }

            var pastResolutionStr   = ((DateTimeResolutionResult)slot.Value).PastResolution;
            var futureResolutionStr = ((DateTimeResolutionResult)slot.Value).FutureResolution;

            if (typeOutput == Constants.SYS_DATETIME_DATETIMEALT && pastResolutionStr.Count > 0)
            {
                typeOutput = DetermineResolutionDateTimeType(pastResolutionStr);
            }

            var resolutionPast   = GenerateResolution(type, pastResolutionStr, mod);
            var resolutionFuture = GenerateResolution(type, futureResolutionStr, mod);

            // If past and future are same, keep only one
            if (resolutionFuture.OrderBy(t => t.Key).Select(t => t.Value)
                .SequenceEqual(resolutionPast.OrderBy(t => t.Key).Select(t => t.Value)))
            {
                if (resolutionPast.Count > 0)
                {
                    AddResolutionFields(res, Constants.Resolve, resolutionPast);
                }
            }
            else
            {
                if (resolutionPast.Count > 0)
                {
                    AddResolutionFields(res, Constants.ResolveToPast, resolutionPast);
                }

                if (resolutionFuture.Count > 0)
                {
                    AddResolutionFields(res, Constants.ResolveToFuture, resolutionFuture);
                }
            }

            // If 'ampm', double our resolution accordingly
            if (!string.IsNullOrEmpty(comment) && comment.Equals(Constants.Comment_AmPm))
            {
                if (res.ContainsKey(Constants.Resolve))
                {
                    ResolveAmpm(res, Constants.Resolve);
                }
                else
                {
                    ResolveAmpm(res, Constants.ResolveToPast);
                    ResolveAmpm(res, Constants.ResolveToFuture);
                }
            }

            // If WeekOf and in CalendarMode, modify the past part of our resolution
            if ((Config.Options & DateTimeOptions.CalendarMode) != 0 &&
                !string.IsNullOrEmpty(comment) && comment.Equals(Constants.Comment_WeekOf))
            {
                ResolveWeekOf(res, Constants.ResolveToPast);
            }

            foreach (var p in res)
            {
                if (p.Value is Dictionary <string, string> dictionary)
                {
                    var value = new Dictionary <string, object>();

                    AddResolutionFields(value, DateTimeResolutionKey.Timex, timex);
                    AddResolutionFields(value, DateTimeResolutionKey.Mod, mod);
                    AddResolutionFields(value, ResolutionKey.Type, typeOutput);
                    AddResolutionFields(value, DateTimeResolutionKey.IsLunar, islunar ? islunar.ToString() : string.Empty);
                    AddResolutionFields(value, DateTimeResolutionKey.List, list);

                    if (hasTimeZone)
                    {
                        AddResolutionFields(value, Constants.TimeZone, val.TimeZoneResolution.Value);
                        AddResolutionFields(value, Constants.TimeZoneText, val.TimeZoneResolution.TimeZoneText);
                        AddResolutionFields(value, Constants.UtcOffsetMinsKey, val.TimeZoneResolution.UtcOffsetMins.ToString());
                    }

                    foreach (var q in dictionary)
                    {
                        if (value.ContainsKey(q.Key))
                        {
                            value[q.Key] = q.Value;
                        }
                        else
                        {
                            value.Add(q.Key, q.Value);
                        }
                    }

                    resolutions.Add(value);
                }
            }

            if (resolutionPast.Count == 0 && resolutionFuture.Count == 0 && val.TimeZoneResolution == null)
            {
                var notResolved = new Dictionary <string, object> {
                    {
                        DateTimeResolutionKey.Timex, timex
                    },
                    {
                        ResolutionKey.Type, typeOutput
                    },
                    {
                        ResolutionKey.Value, "not resolved"
                    }
                };

                resolutions.Add(notResolved);
            }

            return(new SortedDictionary <string, object> {
                { ResolutionKey.ValueSet, resolutions }
            });
        }