/// <summary> /// Creates a character handler to handle the "fraction of a second" specifier (f or F). /// </summary> internal static CharacterHandler <TResult, TBucket> CreateFractionHandler <TResult, TBucket> (int maxCount, NodaFunc <TResult, int> getter, NodaAction <TBucket, int> setter) where TBucket : ParseBucket <TResult> { return((pattern, builder) => { char patternCharacter = pattern.Current; int count = pattern.GetRepeatCount(maxCount); builder.AddField(PatternFields.FractionalSeconds, pattern.Current); builder.AddParseAction((str, bucket) => { int fractionalSeconds; // If the pattern is 'f', we need exactly "count" digits. Otherwise ('F') we need // "up to count" digits. if (!str.ParseFraction(count, maxCount, out fractionalSeconds, patternCharacter == 'f')) { return ParseResult <TResult> .MismatchedNumber(new string(patternCharacter, count)); } // No need to validate the value - we've got an appropriate number of digits, so the range is guaranteed. setter(bucket, fractionalSeconds); return null; }); if (patternCharacter == 'f') { builder.AddFormatRightPad(count, maxCount, getter); } else { builder.AddFormatRightPadTruncate(count, maxCount, getter); } }); }
/// <summary> /// Adds parse and format actions for an "negative only" sign. /// </summary> /// <param name="signSetter">Action to take when to set the given sign within the bucket</param> /// <param name="nonNegativePredicate">Predicate to detect whether the value being formatted is non-negative</param> public void AddNegativeOnlySign(NodaAction <TBucket, bool> signSetter, NodaFunc <TResult, bool> nonNegativePredicate) { string negativeSign = formatInfo.NegativeSign; string positiveSign = formatInfo.PositiveSign; AddParseAction((str, bucket) => { if (str.Match(negativeSign)) { signSetter(bucket, false); return(null); } if (str.Match(positiveSign)) { return(ParseResult <TResult> .PositiveSignInvalid); } signSetter(bucket, true); return(null); }); AddFormatAction((value, builder) => { if (!nonNegativePredicate(value)) { builder.Append(negativeSign); } }); }
internal void AddParseValueAction(int minimumDigits, int maximumDigits, char patternChar, int minimumValue, int maximumValue, NodaAction <TBucket, int> valueSetter) { AddParseAction((cursor, bucket) => { int value; bool negative = cursor.Match('-'); if (negative && minimumValue >= 0) { return(ParseResult <TResult> .UnexpectedNegative); } if (!cursor.ParseDigits(minimumDigits, maximumDigits, out value)) { return(ParseResult <TResult> .MismatchedNumber(new string(patternChar, minimumDigits))); } if (negative) { value = -value; } if (value < minimumValue || value > maximumValue) { return(ParseResult <TResult> .FieldValueOutOfRange(value, patternChar)); } valueSetter(bucket, value); return(null); }); }
/// <summary> /// Creates a character handler for the calendar specifier (c). /// </summary> internal static CharacterHandler <TResult, TBucket> CreateCalendarHandler <TResult, TBucket> (NodaFunc <TResult, CalendarSystem> getter, NodaAction <TBucket, CalendarSystem> setter) where TBucket : ParseBucket <TResult> { return((pattern, builder) => { builder.AddField(PatternFields.Calendar, pattern.Current); builder.AddParseAction((cursor, bucket) => { // TODO(V2.0): (Breaking change, although undocumented.) Potentially make this case-sensitive // as we're parsing IDs. foreach (var id in CalendarSystem.Ids) { if (cursor.MatchCaseInsensitive(id, NodaFormatInfo.InvariantInfo.CompareInfo, true)) { setter(bucket, CalendarSystem.ForId(id)); return null; } } return ParseResult <TResult> .NoMatchingCalendarSystem; }); builder.AddFormatAction((value, sb) => sb.Append(getter(value).Id)); }); }
/// <summary> /// Creates a character handler for the day specifier (d). /// </summary> internal static CharacterHandler <TResult, TBucket> CreateDayHandler <TResult, TBucket> (NodaFunc <TResult, int> dayOfMonthGetter, NodaFunc <TResult, int> dayOfWeekGetter, NodaAction <TBucket, int> dayOfMonthSetter, NodaAction <TBucket, int> dayOfWeekSetter) where TBucket : ParseBucket <TResult> { return((pattern, builder) => { int count = pattern.GetRepeatCount(4); PatternFields field; switch (count) { case 1: case 2: field = PatternFields.DayOfMonth; // Handle real maximum value in the bucket builder.AddParseValueAction(count, 2, pattern.Current, 1, 99, dayOfMonthSetter); builder.AddFormatAction((value, sb) => FormatHelper.LeftPad(dayOfMonthGetter(value), count, sb)); break; case 3: case 4: field = PatternFields.DayOfWeek; var format = builder.FormatInfo; IList <string> textValues = count == 3 ? format.ShortDayNames : format.LongDayNames; builder.AddParseLongestTextAction(pattern.Current, dayOfWeekSetter, format.CompareInfo, textValues); builder.AddFormatAction((value, sb) => sb.Append(textValues[dayOfWeekGetter(value)])); break; default: throw new InvalidOperationException("Invalid count!"); } builder.AddField(field, pattern.Current); }); }
public SteppedPattern(NodaAction <TResult, StringBuilder> formatActions, ParseAction[] parseActions, NodaFunc <TBucket> bucketProvider, PatternFields usedFields) { this.formatActions = formatActions; this.parseActions = parseActions; this.bucketProvider = bucketProvider; this.usedFields = usedFields; }
/// <summary> /// Returns a handler for a zero-padded purely-numeric field specifier, such as "seconds", "minutes", "24-hour", "12-hour" etc. /// </summary> /// <param name="maxCount">Maximum permissable count (usually two)</param> /// <param name="field">Field to remember that we've seen</param> /// <param name="minValue">Minimum valid value for the field (inclusive)</param> /// <param name="maxValue">Maximum value value for the field (inclusive)</param> /// <param name="getter">Delegate to retrieve the field value when formatting</param> /// <param name="setter">Delegate to set the field value into a bucket when parsing</param> /// <returns>The pattern parsing failure, or null on success.</returns> internal static CharacterHandler <TResult, TBucket> HandlePaddedField(int maxCount, PatternFields field, int minValue, int maxValue, NodaFunc <TResult, int> getter, NodaAction <TBucket, int> setter) { return((pattern, builder) => { int count = pattern.GetRepeatCount(maxCount); builder.AddField(field, pattern.Current); builder.AddParseValueAction(count, maxCount, pattern.Current, minValue, maxValue, setter); builder.AddFormatLeftPad(count, getter); }); }
/// <summary> /// Returns a built pattern. This is mostly to keep the API for the builder separate from that of the pattern, /// and for thread safety (publishing a new object, thus leading to a memory barrier). /// Note that this builder *must not* be used after the result has been built. /// </summary> internal IPartialPattern <TResult> Build() { NodaAction <TResult, StringBuilder> formatDelegate = null; foreach (NodaAction <TResult, StringBuilder> formatAction in formatActions) { IPostPatternParseFormatAction postAction = formatAction.Target as IPostPatternParseFormatAction; formatDelegate += postAction == null ? formatAction : postAction.BuildFormatAction(usedFields); } return(new SteppedPattern(formatDelegate, formatOnly ? null : parseActions.ToArray(), bucketProvider, usedFields)); }
/// <summary> /// Creates a character handler for a dot (period) or comma, which have the same meaning. /// Formatting always uses a dot, but parsing will allow a comma instead, to conform with /// ISO-8601. This is *not* culture sensitive. /// </summary> internal static CharacterHandler <TResult, TBucket> CreateCommaDotHandler <TResult, TBucket> (int maxCount, NodaFunc <TResult, int> getter, NodaAction <TBucket, int> setter) where TBucket : ParseBucket <TResult> { return((pattern, builder) => { // Note: Deliberately *not* using the decimal separator of the culture - see issue 21. // If the next part of the pattern is an F, then this decimal separator is effectively optional. // At parse time, we need to check whether we've matched the decimal separator. If we have, match the fractional // seconds part as normal. Otherwise, we continue on to the next parsing token. // At format time, we should always append the decimal separator, and then append using PadRightTruncate. if (pattern.PeekNext() == 'F') { pattern.MoveNext(); int count = pattern.GetRepeatCount(maxCount); builder.AddField(PatternFields.FractionalSeconds, pattern.Current); builder.AddParseAction((valueCursor, bucket) => { // If the next token isn't a dot or comma, we assume // it's part of the next token in the pattern if (!valueCursor.Match('.') && !valueCursor.Match(',')) { return null; } // If there *was* a decimal separator, we should definitely have a number. int fractionalSeconds; // Last argument is false because we don't need *all* the digits to be present if (!valueCursor.ParseFraction(count, maxCount, out fractionalSeconds, false)) { return ParseResult <TResult> .MismatchedNumber(new string('F', count)); } // No need to validate the value - we've got one to three digits, so the range 0-999 is guaranteed. setter(bucket, fractionalSeconds); return null; }); builder.AddFormatAction((localTime, sb) => sb.Append('.')); builder.AddFormatRightPadTruncate(count, maxCount, getter); } else { builder.AddParseAction((str, bucket) => str.Match('.') || str.Match(',') ? null : ParseResult <TResult> .MismatchedCharacter(';')); builder.AddFormatAction((value, sb) => sb.Append('.')); } }); }
/// <summary> /// Adds parse actions for a list of strings, such as days of the week or month names. /// The parsing is performed case-insensitively. All candidates are tested, and only the longest /// match is used. /// TODO: Make this much more efficient in terms of capture... /// </summary> internal void AddParseLongestTextAction(char field, NodaAction <TBucket, int> setter, CompareInfo compareInfo, IList <string> textValues) { AddParseAction((str, bucket) => { int bestIndex = -1; int longestMatch = 0; FindLongestMatch(compareInfo, str, textValues, ref bestIndex, ref longestMatch); if (bestIndex != -1) { setter(bucket, bestIndex); str.Move(str.Index + longestMatch); return(null); } return(ParseResult <TResult> .MismatchedText(field)); }); }
/// <summary> /// Creates a character handler for the month-of-year specifier (M). /// </summary> internal static CharacterHandler <TResult, TBucket> CreateMonthOfYearHandler <TResult, TBucket> (NodaFunc <TResult, int> numberGetter, NodaAction <TBucket, int> textSetter, NodaAction <TBucket, int> numberSetter) where TBucket : ParseBucket <TResult> { return((pattern, builder) => { int count = pattern.GetRepeatCount(4); PatternFields field; switch (count) { case 1: case 2: field = PatternFields.MonthOfYearNumeric; // Handle real maximum value in the bucket builder.AddParseValueAction(count, 2, pattern.Current, 0, 99, numberSetter); builder.AddFormatAction((value, sb) => FormatHelper.LeftPad(numberGetter(value), count, sb)); break; case 3: case 4: field = PatternFields.MonthOfYearText; var format = builder.FormatInfo; IList <string> nonGenitiveTextValues = count == 3 ? format.ShortMonthNames : format.LongMonthNames; IList <string> genitiveTextValues = count == 3 ? format.ShortMonthGenitiveNames : format.LongMonthGenitiveNames; if (nonGenitiveTextValues == genitiveTextValues) { builder.AddParseLongestTextAction(pattern.Current, textSetter, format.CompareInfo, nonGenitiveTextValues); } else { builder.AddParseLongestTextAction(pattern.Current, textSetter, format.CompareInfo, genitiveTextValues, nonGenitiveTextValues); } // Hack: see below builder.AddFormatAction(new MonthFormatActionHolder <TResult, TBucket>(format, count, numberGetter).DummyMethod); break; default: throw new InvalidOperationException("Invalid count!"); } builder.AddField(field, pattern.Current); }); }
/// <summary> /// Adds parse and format actions for a mandatory positive/negative sign. /// </summary> /// <param name="signSetter">Action to take when to set the given sign within the bucket</param> /// <param name="nonNegativePredicate">Predicate to detect whether the value being formatted is non-negative</param> public void AddRequiredSign(NodaAction <TBucket, bool> signSetter, NodaFunc <TResult, bool> nonNegativePredicate) { string negativeSign = formatInfo.NegativeSign; string positiveSign = formatInfo.PositiveSign; AddParseAction((str, bucket) => { if (str.Match(negativeSign)) { signSetter(bucket, false); return(null); } if (str.Match(positiveSign)) { signSetter(bucket, true); return(null); } return(ParseResult <TResult> .MissingSign); }); AddFormatAction((value, sb) => sb.Append(nonNegativePredicate(value) ? positiveSign : negativeSign)); }
internal void AddFormatAction(NodaAction <TResult, StringBuilder> formatAction) { formatActions.Add(formatAction); }
private static void HandleHalfAmPmDesignator <TResult, TBucket> (int count, string specifiedDesignator, int specifiedDesignatorValue, NodaFunc <TResult, int> hourOfDayGetter, NodaAction <TBucket, int> amPmSetter, SteppedPatternBuilder <TResult, TBucket> builder) where TBucket : ParseBucket <TResult> { CompareInfo compareInfo = builder.FormatInfo.CompareInfo; if (count == 1) { string abbreviation = specifiedDesignator.Substring(0, 1); builder.AddParseAction((str, bucket) => { int value = str.MatchCaseInsensitive(abbreviation, compareInfo, true) ? specifiedDesignatorValue : 1 - specifiedDesignatorValue; amPmSetter(bucket, value); return(null); }); builder.AddFormatAction((value, sb) => { // Only append anything if it's the non-empty designator. if (hourOfDayGetter(value) / 12 == specifiedDesignatorValue) { sb.Append(specifiedDesignator[0]); } }); return; } builder.AddParseAction((str, bucket) => { int value = str.MatchCaseInsensitive(specifiedDesignator, compareInfo, true) ? specifiedDesignatorValue : 1 - specifiedDesignatorValue; amPmSetter(bucket, value); return(null); }); builder.AddFormatAction((value, sb) => { // Only append anything if it's the non-empty designator. if (hourOfDayGetter(value) / 12 == specifiedDesignatorValue) { sb.Append(specifiedDesignator); } }); }
internal static CharacterHandler <TResult, TBucket> CreateAmPmHandler <TResult, TBucket> (NodaFunc <TResult, int> hourOfDayGetter, NodaAction <TBucket, int> amPmSetter) where TBucket : ParseBucket <TResult> { return((pattern, builder) => { int count = pattern.GetRepeatCount(2); builder.AddField(PatternFields.AmPm, pattern.Current); string amDesignator = builder.FormatInfo.AMDesignator; string pmDesignator = builder.FormatInfo.PMDesignator; // If we don't have an AM or PM designator, we're nearly done. Set the AM/PM designator // to the special value of 2, meaning "take it from the template". if (amDesignator == "" && pmDesignator == "") { builder.AddParseAction((str, bucket) => { amPmSetter(bucket, 2); return null; }); return; } // Odd scenario (but present in af-ZA for .NET 2) - exactly one of the AM/PM designator is valid. // Delegate to a separate method to keep this clearer... if (amDesignator == "" || pmDesignator == "") { int specifiedDesignatorValue = amDesignator == "" ? 1 : 0; string specifiedDesignator = specifiedDesignatorValue == 1 ? pmDesignator : amDesignator; HandleHalfAmPmDesignator(count, specifiedDesignator, specifiedDesignatorValue, hourOfDayGetter, amPmSetter, builder); return; } CompareInfo compareInfo = builder.FormatInfo.CompareInfo; // Single character designator if (count == 1) { // It's not entirely clear whether this is the right thing to do... there's no nice // way of providing a single-character case-insensitive match. string amFirst = amDesignator.Substring(0, 1); string pmFirst = pmDesignator.Substring(0, 1); builder.AddParseAction((str, bucket) => { if (str.MatchCaseInsensitive(amFirst, compareInfo, true)) { amPmSetter(bucket, 0); return null; } if (str.MatchCaseInsensitive(pmFirst, compareInfo, true)) { amPmSetter(bucket, 1); return null; } return ParseResult <TResult> .MissingAmPmDesignator; }); builder.AddFormatAction((value, sb) => sb.Append(hourOfDayGetter(value) > 11 ? pmDesignator[0] : amDesignator[0])); return; } // Full designator builder.AddParseAction((str, bucket) => { // Could use the "match longest" approach, but with only two it feels a bit silly to build a list... bool pmLongerThanAm = pmDesignator.Length > amDesignator.Length; string longerDesignator = pmLongerThanAm ? pmDesignator : amDesignator; string shorterDesignator = pmLongerThanAm ? amDesignator : pmDesignator; int longerValue = pmLongerThanAm ? 1 : 0; if (str.MatchCaseInsensitive(longerDesignator, compareInfo, true)) { amPmSetter(bucket, longerValue); return null; } if (str.MatchCaseInsensitive(shorterDesignator, compareInfo, true)) { amPmSetter(bucket, 1 - longerValue); return null; } return ParseResult <TResult> .MissingAmPmDesignator; }); builder.AddFormatAction((value, sb) => sb.Append(hourOfDayGetter(value) > 11 ? pmDesignator : amDesignator)); }); }
/// <summary> /// Creates a character handler for the year specifier (y). /// </summary> internal static CharacterHandler <TResult, TBucket> CreateYearHandler <TResult, TBucket> (NodaFunc <TResult, int> centuryGetter, NodaFunc <TResult, int> yearGetter, NodaAction <TBucket, int> setter) where TBucket : ParseBucket <TResult> { return((pattern, builder) => { int count = pattern.GetRepeatCount(5); builder.AddField(PatternFields.Year, pattern.Current); switch (count) { case 1: case 2: builder.AddParseValueAction(count, 2, 'y', -99, 99, setter); builder.AddFormatAction((value, sb) => FormatHelper.LeftPad(centuryGetter(value), count, sb)); // Just remember that we've set this particular field. We can't set it twice as we've already got the Year flag set. builder.AddField(PatternFields.YearTwoDigits, pattern.Current); break; case 3: // Maximum value will be determined later. // Three or more digits (ick). builder.AddParseValueAction(3, 5, 'y', -99999, 99999, setter); builder.AddFormatAction((value, sb) => FormatHelper.LeftPad(yearGetter(value), 3, sb)); break; case 4: // Left-pad to 4 digits when formatting. Parse either exactly 4 or up to 5 digits depending // on the *next* character of the padding. bool parseExactly4 = CheckIfNextCharacterMightBeDigit(pattern); builder.AddParseValueAction(4, parseExactly4 ? 4 : 5, 'y', -99999, 99999, setter); builder.AddFormatAction((value, sb) => FormatHelper.LeftPad(yearGetter(value), 4, sb)); break; case 5: // Maximum value will be determined later. // Note that the *exact* number of digits are required; not just "at least count". builder.AddParseValueAction(count, count, 'y', -99999, 99999, setter); builder.AddFormatAction((value, sb) => FormatHelper.LeftPad(yearGetter(value), 5, sb)); break; default: throw new InvalidOperationException("Bug in Noda Time; invalid count for year went undetected."); } }); }