Skip to content

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 CkDate and CkDateTime method reads a zone or assumeZone id 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.ToDateTime handed back a DateTime not marked as UTC when the entry was missing — the year-one sentinel, or the caller's fallback as it was given — while every other moment the library hands out is UTC.
  • Fixed an issue where CkDictionary.ToDateTime and CkDictionary.ToDateOnly handed back a placeholder fallback, 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 while treatSentinelsAsMissing is true.
  • Fixed an issue where CkDateTime.IsIso8601 could answer true for ISO 8601 text whose offset places the moment before year one, which the readers refuse.
  • Fixed an issue where CkDate.Explain, IsDate and IsPlaceholder, given an assumeZone, 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 other CkDate method leaves a date alone where it is.
  • Fixed an issue where a CkDate method told a zone could 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. Every DateTime it hands out is UTC, InZone excepted. 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 of zone across a daylight-saving change; hours, minutes and seconds are elapsed time — StartOfDay, StartOfNextDay, IsPast, IsFuture, IsBetween, IsDateTime, SameDay and Explain. It throws CkDateTimeException.
  • CkSettings.DefaultZone and CkSettings.DefaultAssumeZone — the application's time zone, set once; both null (UTC) as shipped. zone, whose calendar day counts, is a new parameter on every CkDate method that answers a day and on CkDictionary.ToDateOnly; assumeZone, which clock a zone-less value was written on, now defaults to DefaultAssumeZone — 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.AddWeekdays and WeekdaysBetween — business days, stepping over the caller's holidays; CkSettings.MaxHolidays and CkSettings.MaxWeekdaysToAdd.
  • CkDate.MonthsBetween.
  • CkDate.StartOfWeek and EndOfWeek (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, Divide and Convert (at the caller's rate).
  • CkMoney.Display — an amount for people, with its code; into: DecimalMark.Comma for the comma form.
  • CkMoney.Decimals, NumericCode and CurrencyOf. Every currency parameter now reads the ISO 4217 numeric code as well as the letters.
  • decimals and scale on CkMoney.MinorUnits and FromMinorUnits, and decimals on Format and IsRounded, for a receiver that counts a currency's decimals differently from ISO 4217; CkSettings.MaxMoneyDecimals.
  • CkMoney.Currency takes a textCase.
  • The currencies XCG, VED and KPW.
  • CkCountry reads "Vatican" as VA, beside "Vatican City" and "Holy See".
  • CkMoney.CurrencyOf answers 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 matching PhoneCountry: several countries share a calling code, and telling them apart needs numbering-plan data the library does not carry.
  • CkGeo.ToWkt and CkGeo.ToGeoJson — a point as POINT(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 order CkGeo.IsInBounds takes it: the indexed prefilter for a "within radius" query, with CkGeo.IsWithin as the exact check. Every edge lies CkSettings.BoundsMarginDegrees beyond 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 of CkGeo.Distance and CkGeo.Bearing.

Changed

  • Breaking: CkDate is split in two. ToDateTime, ToDateTimeOrNull, ToTimeOnly, ToTimeOnlyOrNull, Unix, InZone, TimeAgo, HasTime, IsTime and IsIso8601 moved to CkDateTime, unchanged, and throw CkDateTimeException; replace CkDate. with CkDateTime. at each call. Iso8601, Format, IsBefore and IsAfter exist in both classes and answer to the day in CkDate: CkDate.Iso8601 writes "2026-08-31", CkDate.Format refuses a format that writes a time of day, CkDate.IsBefore and IsAfter compare calendar days — call CkDateTime for the moment. CkDictionary.String writes a DateOnly as "2026-08-31".
  • The new zone parameter sits after assumeZone, so a call that passed treatSentinelsAsMissing or fallback by position must now name them.
  • CkMoney.Explain takes an optional currency.
  • 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.Between is capped at CkSettings.MaxHtmlInputLength.
  • CkFile.IsSafeFileName is 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 Unix writes reads back; milliseconds likewise. A millisecond timestamp from before 1978, which was refused, now reads as a far-future moment.
  • CkIntl.Phone, IsPhone, SamePhone, PhoneMask and Explain take a country: the ISO 3166-1 country a national number belongs to, in any spelling CkCountry accepts. The country's trunk prefix comes off and its calling code goes on, so Phone("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 passed fallback, keepLast or throwIfInvalid by position must now name it.
  • CkIntl.Phone reads an RFC 3966 tel: 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.PostalCode and IsPostalCode know 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, Latitude and Longitude — and the validators and …OrNull twins 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 assumeZone in every CkDate method, which for a zone east of UTC gave the day before. A date alone is now the same day on every clock; CkDateTime.ToDateTime still reads it as the zone's midnight.
  • Breaking for one kind of input. CkMoney accepted an amount whose text named a different currency than the currency asked 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, IsBetween and Total no longer compare or add values that name different currencies.
  • Fixed an issue where CkFile.FileName could cut an over-long name in the middle of a character.
  • Fixed an issue where CkDateTime.ToDateTime read 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, StartOfDay and StartOfNextDay, told a zone, could answer a moment at the calendar's edge instead of refusing.
  • Fixed an issue where CkWeb.AddQuery could return a URL longer than Url accepts.
  • Fixed an issue where CkIP.Mask masked 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, Name and Address.
  • Fixed an issue where CkText.ToAscii could hand back markup.
  • Fixed an issue where CkText.Html kept some control characters.
  • Fixed an issue where CkText.Mask left some invisible and control characters visible.
  • Fixed an issue where CkCountry could read a country out of a value that contains digits.
  • Fixed an issue where CkEmail accepted some look-alike domain names.
  • Fixed an issue where CkWeb.IsPublicHost said true for some loopback host names.
  • Fixed an issue where CkIP.IsPublic and CkWeb.IsPublicHost said 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.Value and At; it now arrives as its text.
  • Fixed an issue where CkFile.FromBase64 decoded text that IsBase64 refuses — "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 CkDictionary reads such a value as missing.
  • Fixed an issue where assumeZone shifted 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.Clamp handed back NaN when a bound was NaN; such a bound is refused.
  • Fixed an issue where a wall-clock value that assumeZone places outside the calendar was answered as a moment at the calendar's edge instead of being refused.
  • Fixed an issue where CkText.Mask could 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, and truncate: true is 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.String wrote nothing for a native-sized integer (nint, nuint) and CkSentinel.ToNull let 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 real bool; a word (true, t, yes, y, on and false, 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. FromValue reads all three kinds; FromNumber and FromText one kind each; FromText takes the caller's own trueWords and falseWords; FromValueOrNull, FromNumberOrNull and FromTextOrNull answer null for a missing or unreadable value; IsBool, IsTrue and IsFalse; Format writes a chosen spelling, the new BoolFormat enum (Lowercase, Capitalized, OneZero, YN, YesNo); Explain.
  • CkBoolException, CkSettings.MaxBoolInputLength and CkSettings.DefaultBoolFormat.

Changed

  • Breaking for some inputs. CkDictionary.Bool and CkText.BoolToString read through CkBool, so the two agree: a number is a boolean only when it is exactly 1 or 0 (2 no longer reads as true); numeric text is judged as the number it is ("1.0", "01" read as true); CkDictionary.Bool cleans text like every other reader; text over CkSettings.MaxBoolInputLength is 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 counters WordCount and CharacterCount.
  • 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: an Is… twin of every reader, plus InStates, 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.
  • Explain on 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.FromCsv and FromCsvMaps.
  • CkIP.Anonymize, CkUuid.NewV7, CkCountry.Flag, CkUS.IsPoBox, CkMoney.Multiply and Total, CkWeb.AddQuery and RemoveQuery.
  • CkIntl.PostalCode and IsPostalCode — a postal code for a country, or any exact length; CkIntl.Explain takes country and length.
  • CkSettings.DefaultThrowIfInvalid — the default of every throwIfInvalid parameter; set true in a build of the source, every reader throws on a rejected value.
  • CkText.Truncate takes throwIfInvalid.

Changed

  • The library targets net8.0 (from net10.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 the LICENSE file 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, and String and Address with TextCase.Sentencecase, unify line endings before the tidy; a dotted CkDictionary key walks nested maps when no literal entry holds it; assumeZone applies to a value read by an exact format with dashes after the clock; CkFile.RelativePath rejects a segment that starts with a space; CkUS.RoutingNumber rejects all zeros; CkWeb.Url rejects an empty user marker and a relative path; CkIP.Address rejects an address with a port; the CkNumber.Decimal readers reject a double below the smallest decimal step as out of range.
  • CkDate reads the space-form ISO shape with fractions and an offset.
  • CkNumber reads a boxed Half or BigInteger; Clamp falls back to the lower bound when no fallback is given.

Fixed

  • Fixed an issue where assumeZone was skipped for a value whose exact format puts dashes after the clock.
  • Fixed an issue where CkDate.Age and IsAdult counted a birth carrying a time on the as-of day as after it.
  • Fixed an issue where CkEmail.HasDisplayName did not read a name before an address without brackets.
  • Fixed an issue where keyword matching in CkText.Html, CkIntl.Phone and CkUS.Phone could 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) and CkDictionary (typed values out of any dictionary-shaped bag, JsonElement payloads included).