Changelog — SafeCoder.CommonKit
All notable changes to the library. Versions follow Semantic Versioning. Each version lists what was Added, Changed and Fixed; a breaking change says so.
[1.3.1]
Changed
- Changed how every
CkDateandCkDateTimemethod reads azoneorassumeZoneid the library does not know: it now counts as UTC instead of rejecting the call, so a zone is a preference about a value that is readable on its own, never a reason to refuse it.
Fixed
- Fixed an issue where
CkDictionary.ToDateTimehanded back aDateTimenot marked as UTC when the entry was missing — the year-one sentinel, or the caller'sfallbackas it was given — while every other moment the library hands out is UTC. - Fixed an issue where
CkDictionary.ToDateTimeandCkDictionary.ToDateOnlyhanded back a placeholderfallback, such as 1900-01-01, when the entry was missing, although the same fallback is refused when the entry cannot be read; a placeholder fallback is no fallback whiletreatSentinelsAsMissingis true. - Fixed an issue where
CkDateTime.IsIso8601could answer true for ISO 8601 text whose offset places the moment before year one, which the readers refuse. - Fixed an issue where
CkDate.Explain,IsDateandIsPlaceholder, given anassumeZone, could move a date alone onto that clock and answer for the day before — a placeholder such as 1900-01-01 then passed as a real date — where every otherCkDatemethod leaves a date alone where it is. - Fixed an issue where a
CkDatemethod told azonecould answer the calendar's last day for a moment that falls past the end of year 9999 on that clock — 23:59 UTC on 31 December 9999 read in Kiritimati — instead of refusing it.
[1.3.0]
CkDate is split in two — CkDate for calendar days, the new CkDateTime for moments — a
breaking change for callers of the methods that moved. CkMoney gains the calculations a
payment needs and holds every amount to its currency.
Added
CkDateTime— moments: a date and a time of day. EveryDateTimeit hands out is UTC,InZoneexcepted. Besides the methods that moved into it (see Changed):Add,AddYears,AddMonths,AddDays,AddHours,AddMinutes,AddSeconds— years, months and days keep the time of day, on the wall clock ofzoneacross a daylight-saving change; hours, minutes and seconds are elapsed time —StartOfDay,StartOfNextDay,IsPast,IsFuture,IsBetween,IsDateTime,SameDayandExplain. It throwsCkDateTimeException.CkSettings.DefaultZoneandCkSettings.DefaultAssumeZone— the application's time zone, set once; both null (UTC) as shipped.zone, whose calendar day counts, is a new parameter on everyCkDatemethod that answers a day and onCkDictionary.ToDateOnly;assumeZone, which clock a zone-less value was written on, now defaults toDefaultAssumeZone— set it only where the database stores local time.CkDate.Add,AddDays,AddMonths,AddYears— the day held to the end of a shorter month.CkDate.AddWeekdaysandWeekdaysBetween— business days, stepping over the caller'sholidays;CkSettings.MaxHolidaysandCkSettings.MaxWeekdaysToAdd.CkDate.MonthsBetween.CkDate.StartOfWeekandEndOfWeek(firstDay, Monday by default),StartOfQuarter,EndOfQuarter,StartOfYear,EndOfYear.CkDate.WeekOfYear— the ISO 8601 week.CkMoney.Allocate— an amount shared by ratio, the parts always summing to the whole.CkMoney.Subtract,DivideandConvert(at the caller's rate).CkMoney.Display— an amount for people, with its code;into: DecimalMark.Commafor the comma form.CkMoney.Decimals,NumericCodeandCurrencyOf. Everycurrencyparameter now reads the ISO 4217 numeric code as well as the letters.decimalsandscaleonCkMoney.MinorUnitsandFromMinorUnits, anddecimalsonFormatandIsRounded, for a receiver that counts a currency's decimals differently from ISO 4217;CkSettings.MaxMoneyDecimals.CkMoney.Currencytakes atextCase.- The currencies
XCG,VEDandKPW. CkCountryreads "Vatican" as VA, beside "Vatican City" and "Holy See".CkMoney.CurrencyOfanswers ILS for Palestine and GBP for South Georgia, the currencies in everyday use where ISO 4217 lists none; Antarctica alone has none.CkIntl.CallingCode— the country calling code of a phone number, as digits without the "+":"41"for"+41 44 668 18 00","1"for every number of the North American plan. There is no matchingPhoneCountry: several countries share a calling code, and telling them apart needs numbering-plan data the library does not carry.CkGeo.ToWktandCkGeo.ToGeoJson— a point asPOINT(lon lat)and as{"type":"Point","coordinates":[lon,lat]}. Both formats put the longitude first.CkGeo.BoundsAround— the latitude and longitude box around a centre, in the orderCkGeo.IsInBoundstakes it: the indexed prefilter for a "within radius" query, withCkGeo.IsWithinas the exact check. Every edge liesCkSettings.BoundsMarginDegreesbeyond the circle, so a point exactly on it is never dropped by the prefilter.CkGeo.Destination— the point a bearing and a distance lead to from a starting point, the inverse ofCkGeo.DistanceandCkGeo.Bearing.
Changed
- Breaking:
CkDateis split in two.ToDateTime,ToDateTimeOrNull,ToTimeOnly,ToTimeOnlyOrNull,Unix,InZone,TimeAgo,HasTime,IsTimeandIsIso8601moved toCkDateTime, unchanged, and throwCkDateTimeException; replaceCkDate.withCkDateTime.at each call.Iso8601,Format,IsBeforeandIsAfterexist in both classes and answer to the day inCkDate:CkDate.Iso8601writes"2026-08-31",CkDate.Formatrefuses a format that writes a time of day,CkDate.IsBeforeandIsAftercompare calendar days — callCkDateTimefor the moment.CkDictionary.Stringwrites aDateOnlyas"2026-08-31". - The new
zoneparameter sits afterassumeZone, so a call that passedtreatSentinelsAsMissingorfallbackby position must now name them. CkMoney.Explaintakes an optionalcurrency.- A repeated key in a JSON payload or a pair list: the last value wins, in every bag and every keyed read, as it always did in a form body.
CkText.Betweenis capped atCkSettings.MaxHtmlInputLength.CkFile.IsSafeFileNameis false for a name that contains an invisible character.- A bare number reads as Unix seconds up to the end of year 9999, where the readers stopped at
the end of 2099, so every moment
Unixwrites reads back; milliseconds likewise. A millisecond timestamp from before 1978, which was refused, now reads as a far-future moment. CkIntl.Phone,IsPhone,SamePhone,PhoneMaskandExplaintake acountry: the ISO 3166-1 country a national number belongs to, in any spellingCkCountryaccepts. The country's trunk prefix comes off and its calling code goes on, soPhone("020 7946 0958", country: "GB")is"+442079460958". A value that declares its own country code is read as before and the country is ignored. The new parameter sits after the value, so a call that passedfallback,keepLastorthrowIfInvalidby position must now name it.CkIntl.Phonereads an RFC 3966tel:value —"tel:+41-44-668-18-00","TEL:+41446681800;ext=12". The scheme and everything from the first;are dropped, the way a trailing extension already was.CkIntl.PostalCodeandIsPostalCodeknow 181 countries and territories, where they knew 52. A code is written the way its country writes it, including the letters some countries print in front —"AD500","AZ 1000","MD-2001". The United States' territories and the freely associated states are read as the US ZIP codes they are.CkGeo.TryParse,LatitudeandLongitude— and the validators and…OrNulltwins that read through them — also read a point in well-known text,"POINT(-74.006 40.7128)", its extended form with SRID 4326, and a GeoJSON Point object. Those formats put the longitude first, and the readers swap it back. Any SRID other than 4326 is refused.
Fixed
- Breaking for one kind of call. A date with no time of day was shifted by
assumeZonein everyCkDatemethod, which for a zone east of UTC gave the day before. A date alone is now the same day on every clock;CkDateTime.ToDateTimestill reads it as the zone's midnight. - Breaking for one kind of input.
CkMoneyaccepted an amount whose text named a different currency than thecurrencyasked for, and scaled it by the wrong currency's decimals. Such a value is now refused, with a reason that names both codes. Text that names the currency asked for, or none, reads as before. - Text that names two different currencies is no longer read as an amount.
CkMoney.SameAmount,IsAtLeast,IsAtMost,IsBetweenandTotalno longer compare or add values that name different currencies.- Fixed an issue where
CkFile.FileNamecould cut an over-long name in the middle of a character. - Fixed an issue where
CkDateTime.ToDateTimeread a timestamp whose offset places it outside the calendar as a wrong moment instead of refusing it. - Fixed an issue where
CkDateTime.Add,AddDays,AddMonths,AddYears,StartOfDayandStartOfNextDay, told azone, could answer a moment at the calendar's edge instead of refusing. - Fixed an issue where
CkWeb.AddQuerycould return a URL longer thanUrlaccepts. - Fixed an issue where
CkIP.Maskmasked an IPv4 address written in IPv6 form as"0:0:0:***". - Fixed an issue where an incomplete character could pass through the cleaners unchanged; it is now replaced by U+FFFD.
- Fixed an issue where some markup could survive
CkText.String,Html,NameandAddress. - Fixed an issue where
CkText.ToAsciicould hand back markup. - Fixed an issue where
CkText.Htmlkept some control characters. - Fixed an issue where
CkText.Maskleft some invisible and control characters visible. - Fixed an issue where
CkCountrycould read a country out of a value that contains digits. - Fixed an issue where
CkEmailaccepted some look-alike domain names. - Fixed an issue where
CkWeb.IsPublicHostsaid true for some loopback host names. - Fixed an issue where
CkIP.IsPublicandCkWeb.IsPublicHostsaid true for some internal addresses written in IPv6 forms. - Fixed an issue where a JSON number too large for a double reached a bag as infinity through
CkDictionary.ValueandAt; it now arrives as its text. - Fixed an issue where
CkFile.FromBase64decoded text thatIsBase64refuses —"no","US", a date — into bytes. It now refuses what the validator refuses. - Fixed an issue where a JSON payload holding an escape for half of a surrogate pair was refused
with an internal error message as the reason; it is refused as "not valid JSON", as the
platform's own reader refuses it, and a raw element handed to
CkDictionaryreads such a value as missing. - Fixed an issue where
assumeZoneshifted a moment whose offset is written with a single-digit hour,+2:00, as if it had declared no zone. - Fixed an issue where
CkNumber.Clamphanded back NaN when a bound was NaN; such a bound is refused. - Fixed an issue where a wall-clock value that
assumeZoneplaces outside the calendar was answered as a moment at the calendar's edge instead of being refused. - Fixed an issue where
CkText.Maskcould show an emoji among the trailing characters it keeps visible; only letters and digits are shown there.
[1.2.1]
Fixed
- Fixed an issue where a value padded with whitespace past its reader's length limit — a
CHAR(255)column, a fixed-width export — was rejected by every strict reader, with a reason that named the wrong cause. The limit is now measured with outer whitespace set aside; a value over it is still rejected, andtruncate: trueis unaffected. This takes back the padding line of 1.2.0's breaking changes: a padded "yes" reads as true again. - Fixed an issue where
CkDictionary.Stringwrote nothing for a native-sized integer (nint,nuint) andCkSentinel.ToNulllet their zero through.
[1.2.0]
Booleans get a class of their own and the library one boolean rule: CkBool reads, validates,
explains and writes them, and the two methods that read booleans before it now read through it.
Added
CkBool— a realbool; a word (true,t,yes,y,onandfalse,f,no,n,off, any casing); or a number that is exactly 1 or 0, as a number or as numeric text. Anything else is unreadable, never false.FromValuereads all three kinds;FromNumberandFromTextone kind each;FromTexttakes the caller's owntrueWordsandfalseWords;FromValueOrNull,FromNumberOrNullandFromTextOrNullanswernullfor a missing or unreadable value;IsBool,IsTrueandIsFalse;Formatwrites a chosen spelling, the newBoolFormatenum (Lowercase,Capitalized,OneZero,YN,YesNo);Explain.CkBoolException,CkSettings.MaxBoolInputLengthandCkSettings.DefaultBoolFormat.
Changed
- Breaking for some inputs.
CkDictionary.BoolandCkText.BoolToStringread throughCkBool, so the two agree: a number is a boolean only when it is exactly 1 or 0 (2no longer reads as true); numeric text is judged as the number it is ("1.0","01"read as true);CkDictionary.Boolcleans text like every other reader; text overCkSettings.MaxBoolInputLengthis rejected; the reason reads "not a recognised true/false value".
[1.1.0]
A verdict beside every reader, an Explain on every class, the everyday helpers, a postal-code
reader, and the library on .NET 8 for every host.
Added
- Validators on every class, for code that needs a verdict rather than a cleaned value:
CkText.IsEmpty,HasLength,StartsWith,EndsWith,Contains,ContainsAny,InList,IsDigits,IsLetters,IsAlphanumeric,IsAscii,IsUpper,IsLower,Matches; the countersWordCountandCharacterCount.CkCountry.IsCountry,IsAlpha2,IsAlpha3,InCountries,SameCountry.CkDate.IsDate,IsTime,IsIso8601,HasTime,IsPlaceholder,IsBefore,IsAfter,IsPast,IsFuture,IsToday,IsAdult,IsWeekday,IsWeekend,IsLeapYear;CkSettings.DefaultAdultAge.CkWeb.IsUrl,InDomains,SameOrigin,SameUrl,IsIpHost,HasPort,HasQuery.CkUuid.IsUuid,IsNil,IsCanonical,IsStandard,IsVersion,IsFromName,SameUuid.CkUS: anIs…twin of every reader, plusInStates,SamePhone,SameTin,HasPlus4,IsTollFree,IsTerritory,IsMilitary,InIcd10Ranges,InCptRanges.CkNumber.IsNumber,IsInteger,IsPositive,IsNegative,IsZero,IsBetween,IsAtLeast,IsAtMost,HasDecimals,IsEven,IsOdd,Fits<T>;CkSettings.MaxDecimalPlaces.CkMoney.IsAmount,IsCurrency,InCurrencies,IsRounded,IsPositive,IsNegative,IsZero,IsBetween,IsAtLeast,IsAtMost,SameAmount,HasCurrency.CkIP.IsAddress,IsIPv4,IsIPv6,IsInternal,IsLoopback,IsMapped,IsRange,SameAddress.CkIntl.IsPhone,IsIban,IsBic,SamePhone,SameIban,IbanMatchesBic,InCallingCodes,IbanInCountries.CkGeo.IsLatitude,IsLongitude,IsCoordinate,IsInBounds,IsPlaceholder.CkFile.IsSafeFileName,IsRelativePath,IsBase64,IsDataUri,InMediaTypes,IsWithinSize.CkEmail.IsAddress,HasTag,SameDomain,IsFreeMail,HasDisplayName.Explainon every class with a reader — the reader's reason as text,""when the value passes, never throwing.CkDictionary: dotted keys read paths through nested maps in every reader;At,Has,MissingKeys.CkDate.InZone,DaysBetween,StartOfMonth,EndOfMonth,Quarter,TimeAgo.CkNumber.Clamp<T>,Round,Divide.CkText.ToAscii,Initials,Between,Plural,Ordinal.CkFile.Combine,SizeToString,MediaType,ToDataUri.CkPayload.FromCsvandFromCsvMaps.CkIP.Anonymize,CkUuid.NewV7,CkCountry.Flag,CkUS.IsPoBox,CkMoney.MultiplyandTotal,CkWeb.AddQueryandRemoveQuery.CkIntl.PostalCodeandIsPostalCode— a postal code for a country, or any exactlength;CkIntl.Explaintakescountryandlength.CkSettings.DefaultThrowIfInvalid— the default of everythrowIfInvalidparameter; set true in a build of the source, every reader throws on a rejected value.CkText.TruncatetakesthrowIfInvalid.
Changed
- The library targets
net8.0(fromnet10.0), so every .NET 8+ application can consume it; the tests run on both. - One package dependency, the first:
TimeZoneConverter, which resolves time-zone ids on every host. - The assembly's informational version is
<version>+<flavour>; the library is licensed under MIT, with theLICENSEfile in the package. - Breaking for some inputs. The cleaning pipeline composes decomposed accents (Unicode
NFC) on the way in, in every reader of every class;
CkText.Html, andStringandAddresswithTextCase.Sentencecase, unify line endings before the tidy; a dottedCkDictionarykey walks nested maps when no literal entry holds it;assumeZoneapplies to a value read by an exactformatwith dashes after the clock;CkFile.RelativePathrejects a segment that starts with a space;CkUS.RoutingNumberrejects all zeros;CkWeb.Urlrejects an empty user marker and a relative path;CkIP.Addressrejects an address with a port; theCkNumber.Decimalreaders reject a double below the smallest decimal step as out of range. CkDatereads the space-form ISO shape with fractions and an offset.CkNumberreads a boxedHalforBigInteger;Clampfalls back to the lower bound when nofallbackis given.
Fixed
- Fixed an issue where
assumeZonewas skipped for a value whose exactformatputs dashes after the clock. - Fixed an issue where
CkDate.AgeandIsAdultcounted a birth carrying a time on the as-of day as after it. - Fixed an issue where
CkEmail.HasDisplayNamedid not read a name before an address without brackets. - Fixed an issue where keyword matching in
CkText.Html,CkIntl.PhoneandCkUS.Phonecould follow the process culture.
[1.0.0]
The initial release: sixteen static helper classes that read, clean, validate and convert
values on their way between the wire, the application and the database. Nothing throws unless
asked — throwIfInvalid: true raises the class's own Ck*Exception, carrying the value, the
expectation and the reason — and by default a value that cannot be used comes back as the
caller's fallback, an empty value or a declared sentinel instead. The …OrNull readers never
throw and take no flag: null is their answer for any error.
- Text and identity:
CkText,CkEmail,CkUS,CkIntl,CkCountry,CkUuid,CkIP. - Numbers, money, time and place:
CkNumber,CkMoney,CkDate,CkGeo. - Files and the web:
CkFile,CkWeb. - Payload intake and extraction:
CkPayload(wire text — JSON, form bodies, query strings — into one standard dictionary shape) andCkDictionary(typed values out of any dictionary-shaped bag,JsonElementpayloads included).