/// <summary>
        /// Transforms a format to a format that only allows the characters
        /// h, m, s, t, H and the defined TimeSeparators (: and .).
        /// Also takes into account the rule that a single TimeCharacter should
        /// be followed by a space.
        /// </summary>
        /// <param name="format">The format that needs to be transformed.</param>
        /// <returns>A format containing only the expected characters.</returns>
        protected virtual string GetTransformedFormat(string format)
        {
            // make sure everything that we do not understand is not part of the
            // format.
            // This method was discussed with BCL team.
            string transformed = new string(format.ToCharArray()
                                            .Where(c => Char.IsWhiteSpace(c) ||
                                                   TimeSeparators.Contains(c) ||
                                                   TimeChars.Contains(c))
                                            .Select(c => c).ToArray()).Trim();

            // feature: DateTime will not parse single characters.
            // the documented workaround is to add a space.
            if (transformed.Length == 1)
            {
                transformed = new string(new[] { transformed[0], ' ' });
            }

            return(transformed);
        }
        /// <summary>
        /// Parses a string into a DateTime using the specified ITimeFormat instance
        /// and TimeParsers and returns a value that indicates whether the conversion
        /// succeeded.
        /// </summary>
        /// <param name="mappedText">The text that was entered by the user.</param>
        /// <param name="timeFormat">The TimeFormat instance used to supply
        /// formats.</param>
        /// <param name="timeParsers">The time parsers.</param>
        /// <param name="result">A DateTime with a correctly set time part.</param>
        /// <returns>
        /// True, if the time was parsed correctly, false if the time was not
        /// parsed.
        /// </returns>
        /// <remarks>The date part of the DateTime is irrelevant and will be
        /// overwritten by the current date.
        /// </remarks>
        public bool TryParseTime(string mappedText, ITimeFormat timeFormat, IEnumerable <TimeParser> timeParsers, out DateTime?result)
        {
            result = null;
            if (string.IsNullOrEmpty(mappedText))
            {
                return(true);
            }
            string value = new string(mappedText.ToCharArray().Select(c => TimeSeparators.Contains(c) ? c : MapCharacterToDigit(c)).ToArray());

            if (timeFormat != null)
            {
                DateTime realResult;
                // try using formats.
                if (DateTime.TryParseExact(
                        value,
                        timeFormat.GetTimeParseFormats(ActualCulture).Select(s => GetTransformedFormat(s)).ToArray(),
                        ActualCulture,
                        DateTimeStyles.None,
                        out realResult))
                {
                    result = realResult;
                    return(true);
                }
            }

            // try using custom collection of parsers.
            TimeParserCollection timeParserCollection = new TimeParserCollection(GetActualTimeParsers(timeParsers));
            DateTime?            parsedResult;

            if (timeParserCollection.TryParse(mappedText, ActualCulture, out parsedResult))
            {
                result = parsedResult;
                return(true);
            }

            return(false);
        }
        /// <summary>
        /// Gets the position for a time unit in a string that can be parsed by
        /// the specified ITimeFormat.
        /// </summary>
        /// <param name="text">The text that represents a DateTime.</param>
        /// <param name="timeSpan">The time span that is searched for.</param>
        /// <param name="timeFormat">The time format that describes how this text can be
        /// parsed to a DateTime.</param>
        /// <returns>
        /// The position in the text that corresponds to the TimeSpan or
        /// -1 if none was found.
        /// </returns>
        public virtual int GetTextPositionForTimeUnit(string text, TimeSpan timeSpan, ITimeFormat timeFormat)
        {
            if (string.IsNullOrEmpty(text))
            {
                return(-1);
            }

            int designatorStartIndex = GetDesignatorTextPositionStart(text);
            int designatorEndIndex   = GetDesignatorTextPositionEnd(text, designatorStartIndex);

            if (timeSpan == TimeSpan.FromHours(12))
            {
                return(designatorStartIndex);
            }

            for (int i = 0; i < text.Length; i++)
            {
                if (i > designatorStartIndex && i < designatorEndIndex)
                {
                    continue;
                }

                char c = text[i];
                if (Char.IsWhiteSpace(c) || TimeSeparators.Contains(c))
                {
                    continue;
                }

                if (timeSpan == GetTimeUnitAtTextPosition(text, i, timeFormat))
                {
                    return(i);
                }
            }

            return(-1);
        }
        public virtual TimeSpan GetTimeUnitAtTextPosition(string text, int textPosition, ITimeFormat timeFormat)
        {
            // validate
            if (string.IsNullOrEmpty(text) || textPosition < 0 || textPosition > text.Length)
            {
                return(new TimeSpan());
            }

            if (timeFormat == null)
            {
                throw new ArgumentNullException("timeFormat");
            }

            // the position that is taken into account is always the
            // character that comes after the caret. If we are at the
            // end of the text, we will want to parse the character
            // before.

            // if the caret is currently at a timeseperator (:, or .)
            // also use the previous character.
            if (textPosition == text.Length || TimeSeparators.Contains(text[textPosition]))
            {
                // act on character before
                return(GetTimeUnitAtTextPosition(text, (textPosition - 1), timeFormat));
            }

            // if the caret is at a whitespace, look around for the first real character
            if (Char.IsWhiteSpace(text[textPosition]))
            {
                int offset = 1;
                while (textPosition + offset < text.Length || textPosition - offset >= 0)
                {
                    if (textPosition - offset >= 0 && !Char.IsWhiteSpace(text[textPosition - offset]))
                    {
                        return(GetTimeUnitAtTextPosition(text, textPosition - offset, timeFormat));
                    }
                    if (textPosition + offset < text.Length && !Char.IsWhiteSpace(text[textPosition + offset]))
                    {
                        return(GetTimeUnitAtTextPosition(text, textPosition + offset, timeFormat));
                    }

                    offset++;
                }
            }

            #region handle am/pm flip and return
            // find out information about usage of AM/PM
            int designatorStartIndex = GetDesignatorTextPositionStart(text);
            int designatorEndIndex   = GetDesignatorTextPositionEnd(text, designatorStartIndex);

            if (textPosition >= designatorStartIndex && textPosition < designatorEndIndex)
            {
                return(TimeSpan.FromHours(12));
            }
            #endregion

            // find out the timespan that the spin has effect on
            // by clearing all the numbers (set to 0) and only setting to 1
            // at the caretposition. The remaining timespan can be used to
            // determine how to increment.
            StringBuilder timeSpanBuilder = new StringBuilder(text.Length);

            #region Determine timespan
            for (int i = 0; i < text.Length; i++)
            {
                char c = text[i];

                // fill with 0
                timeSpanBuilder.Append('0');

                // set a 1
                timeSpanBuilder[i] = i == textPosition ? '1' : '0';

                // copy over separator
                if (TimeSeparators.Contains(c))
                {
                    timeSpanBuilder[i] = c;
                }

                // copy over designator
                if (i >= designatorStartIndex && i < designatorEndIndex)
                {
                    timeSpanBuilder[i] = c;
                }

                // retain white space
                if (char.IsWhiteSpace(c))
                {
                    timeSpanBuilder[i] = c;
                }
            }
            #endregion

            string NulledTimeAM = timeSpanBuilder.ToString();
            if (!string.IsNullOrEmpty(PMDesignator))
            {
                NulledTimeAM = NulledTimeAM.Replace(PMDesignator, AMDesignator);
            }
            DateTime?spinnedTime;
            if (TryParseTime(NulledTimeAM, timeFormat, null, out spinnedTime) && spinnedTime.HasValue)
            {
                // behavior is special for hours.
                // we do not do contextual spinning on the hours, since this
                // turns out to be too confusing.
                if (spinnedTime.Value.TimeOfDay.Hours == 10)
                {
                    return(TimeSpan.FromHours(1));
                }
                return(spinnedTime.Value.TimeOfDay);
            }
            else
            {
                return(new TimeSpan());
            }
        }