Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions JustDummies/AnyBoolean.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ namespace JustDummies;
/// </summary>
public sealed class AnyBoolean : IAny<bool>, IHasRandomSource, ICardinalityHint<bool> {

/// <summary>How many values <see cref="bool" /> has: <c>false</c> and <c>true</c>, and nothing else.</summary>
private const int BooleanValueCount = 2;

/// <summary>How many values a pin leaves producible — the one it fixed.</summary>
private const int PinnedCardinality = 1;

#region Statics members declarations

internal static AnyBoolean Create(RandomSource source) {
Expand Down Expand Up @@ -38,7 +44,7 @@ private AnyBoolean(RandomSource source, bool? pinned, ConstraintCall? pinnedCons
RandomSource? IHasRandomSource.Source => _source;

// Two distinct values unless a pin has already fixed one of them.
long? ICardinalityHint<bool>.DistinctCardinality => _pinned is null ? 2 : 1;
long? ICardinalityHint<bool>.DistinctCardinality => _pinned is null ? BooleanValueCount : PinnedCardinality;

// A pin narrows the domain to that single value; unpinned, both booleans are producible.
bool ICardinalityHint<bool>.Contains(bool value) => _pinned is not bool pinned || pinned == value;
Expand Down Expand Up @@ -70,7 +76,7 @@ public AnyBoolean DifferentFrom(bool value) {

/// <inheritdoc />
public bool Generate() {
return _pinned ?? _source.Current.Next(2) == 0;
return _pinned ?? _source.Current.Next(BooleanValueCount) == 0;
}

private AnyBoolean Pin(bool value, ConstraintCall applying) {
Expand Down
5 changes: 4 additions & 1 deletion JustDummies/AnyDecimal.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ namespace JustDummies;
/// </summary>
public sealed class AnyDecimal : IAny<decimal>, IHasRandomSource, ICardinalityHint<decimal> {

/// <summary>The fewest decimal places <see cref="WithScale" /> accepts — a whole number, with no fractional part.</summary>
private const int MinScale = 0;

#region Statics members declarations

internal static AnyDecimal Create(RandomSource source) {
Expand Down Expand Up @@ -136,7 +139,7 @@ public AnyDecimal Between(decimal minimum, decimal maximum) {
/// <exception cref="ArgumentOutOfRangeException">Thrown when <paramref name="scale" /> is outside the range [0, 28].</exception>
/// <exception cref="ConflictingAnyConstraintException">Thrown when the constraint contradicts a constraint already declared.</exception>
public AnyDecimal WithScale(int scale) {
if (scale < 0 || scale > 28) { throw new ArgumentOutOfRangeException(nameof(scale), scale, "The scale must be in the inclusive range [0, 28]."); }
if (scale < MinScale || scale > DecimalIntervalSpec.MaxScale) { throw new ArgumentOutOfRangeException(nameof(scale), scale, $"The scale must be in the inclusive range [{MinScale}, {DecimalIntervalSpec.MaxScale}]."); }

return new AnyDecimal(_source, _spec.WithScale(scale, ConstraintCall.Of(nameof(WithScale), scale.ToString(CultureInfo.InvariantCulture))));
}
Expand Down
10 changes: 8 additions & 2 deletions JustDummies/AnyGuid.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ namespace JustDummies;
/// </summary>
public sealed class AnyGuid : IAny<Guid>, IHasRandomSource, ICardinalityHint<Guid> {

/// <summary>How many bytes a <see cref="Guid" /> is made of — its 128 bits, which a draw fills whole.</summary>
private const int GuidByteCount = 16;

/// <summary>How many values a pin leaves producible — the one it fixed.</summary>
private const int PinnedCardinality = 1;

#region Statics members declarations

internal static AnyGuid Create(RandomSource source) {
Expand Down Expand Up @@ -68,7 +74,7 @@ private AnyGuid(RandomSource source, Guid? pinned, ConstraintCall? pinnedConstra
RandomSource? IHasRandomSource.Source => _source;

// Pinned to a single value, or bounded by an allow-list; otherwise the domain is effectively unbounded.
long? ICardinalityHint<Guid>.DistinctCardinality => _pinned is not null ? 1 : _effectiveAllowed?.Count;
long? ICardinalityHint<Guid>.DistinctCardinality => _pinned is not null ? PinnedCardinality : _effectiveAllowed?.Count;

// Mirrors Generate: the pin, then the allow-list, then the full space minus the exclusions.
bool ICardinalityHint<Guid>.Contains(Guid value) {
Expand Down Expand Up @@ -144,7 +150,7 @@ public Guid Generate() {
return _effectiveAllowed[random.Next(_effectiveAllowed.Count)];
}

byte[] bytes = new byte[16];
byte[] bytes = new byte[GuidByteCount];
random.NextBytes(bytes);
Guid candidate = new(bytes);
// Colliding with an excluded identifier has probability |excluded| / 2^128 per draw. On a collision,
Expand Down
12 changes: 10 additions & 2 deletions JustDummies/AnyInt128.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ namespace JustDummies;
/// </summary>
public sealed class AnyInt128 : IAny<Int128>, IHasRandomSource, ICardinalityHint<Int128> {

/// <summary>
/// The bit that tells a negative <see cref="Int128" /> from a non-negative one. Flipping it maps a signed
/// value onto its order-preserving ordinal and back — the 128-bit twin of <see cref="OrdinalMapping" />'s
/// 64-bit mapping. It is <c>static readonly</c> rather than <c>const</c> because C# has no constant of a
/// user-defined type such as <see cref="UInt128" />.
/// </summary>
private static readonly UInt128 SignBit = UInt128.One << 127;

#region Statics members declarations

internal static AnyInt128 Create(RandomSource source) {
Expand All @@ -25,11 +33,11 @@ internal static AnyInt128 Create(RandomSource source) {
}

private static UInt128 Ord(Int128 value) {
return unchecked((UInt128)value) ^ (UInt128.One << 127);
return unchecked((UInt128)value) ^ SignBit;
}

private static Int128 Val(UInt128 ordinal) {
return unchecked((Int128)(ordinal ^ (UInt128.One << 127)));
return unchecked((Int128)(ordinal ^ SignBit));
}

private static string V(Int128 value) {
Expand Down
16 changes: 14 additions & 2 deletions JustDummies/CollectionState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,18 @@ namespace JustDummies;
/// <typeparam name="T">The element type.</typeparam>
internal sealed class CollectionState<T> {

// The three numbers the exhaustion budget is built from. They bound how long a dedup-draw may keep colliding
// before it reports a shortfall, and nothing outside ExhaustionBudget reads them.

/// <summary>How many consecutive collisions each value of a known finite domain is allowed to cost.</summary>
private const long CollisionsPerValue = 64L;

/// <summary>The cardinality up to which the budget scales with the domain rather than with the requested count.</summary>
private const long ScalableCardinality = 1_000_000L;

/// <summary>The floor the budget never drops below, whatever the domain and the count work out to.</summary>
private const long MinimumBudget = 10_000L;

#region Statics members declarations

internal static CollectionState<T> Create(IAny<T> item, bool distinct, IEqualityComparer<T>? comparer) {
Expand Down Expand Up @@ -268,9 +280,9 @@ private int ExhaustionBudget(int target) {
// floor that collisions only reach if the domain is unexpectedly small (for example a comparer that
// merges most values). Either way the fill is bounded — never an unbounded retry loop.
long cardinality = _itemCardinality ?? long.MaxValue;
long bounded = cardinality <= 1_000_000L ? 64L * cardinality : 64L * target;
long bounded = cardinality <= ScalableCardinality ? CollisionsPerValue * cardinality : CollisionsPerValue * target;

return (int)Math.Min(Math.Max(bounded, 10_000L), int.MaxValue);
return (int)Math.Min(Math.Max(bounded, MinimumBudget), int.MaxValue);
}

private static AnyGenerationException Exhausted(RandomSource source, int reached, int target, string what, IAny<T> culprit) {
Expand Down
10 changes: 8 additions & 2 deletions JustDummies/ConstraintClaim.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ namespace JustDummies;
[ValueObject]
internal sealed class ConstraintClaim : IEquatable<ConstraintClaim> {

/// <summary>
/// The odd prime each field's hash is multiplied by before the next is folded in, so that two fields swapping
/// values do not collide. Its exact value carries no meaning beyond being odd and prime.
/// </summary>
private const int HashMultiplier = 397;

#region Statics members declarations

/// <summary>
Expand Down Expand Up @@ -120,9 +126,9 @@ public override bool Equals(object? obj) {
public override int GetHashCode() {
unchecked {
int hash = StringComparer.Ordinal.GetHashCode(Subject);
hash = (hash * 397) ^ StringComparer.Ordinal.GetHashCode(Claims);
hash = (hash * HashMultiplier) ^ StringComparer.Ordinal.GetHashCode(Claims);

return (hash * 397) ^ (Constraint?.GetHashCode() ?? 0);
return (hash * HashMultiplier) ^ (Constraint?.GetHashCode() ?? 0);
}
}

Expand Down
14 changes: 10 additions & 4 deletions JustDummies/DecimalIntervalSpec.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ internal sealed class DecimalIntervalSpec {
private const int NoScale = -1;
private const int NudgeBudget = 128;

/// <summary>The most decimal places a <see cref="decimal" /> carries — the widest scale its 96-bit mantissa allows.</summary>
internal const int MaxScale = 28;

/// <summary>How many bytes that mantissa spans: 96 bits, which <see cref="BitConverter" /> reads back as three limbs.</summary>
private const int MantissaByteCount = 3 * sizeof(int);

private static readonly decimal SmallestStep = 0.0000000000000000000000000001m;
private static readonly decimal MaxFraction = 7.9228162514264337593543950335m;

Expand Down Expand Up @@ -236,13 +242,13 @@ internal decimal Generate(RandomSource source) {
// A uniform fraction in [0, 1] over the full 96-bit mantissa scale. NextBytes fills all three
// limbs — including each limb's top bit, which three non-negative Random.Next() draws would pin
// to zero, capping the fraction near 0.5 and leaving the upper half of every range unreachable.
byte[] mantissa = new byte[12];
byte[] mantissa = new byte[MantissaByteCount];
random.NextBytes(mantissa);
decimal fraction = new decimal(
BitConverter.ToInt32(mantissa, 0),
BitConverter.ToInt32(mantissa, 4),
BitConverter.ToInt32(mantissa, 8),
false, 28) / MaxFraction;
BitConverter.ToInt32(mantissa, sizeof(int)),
BitConverter.ToInt32(mantissa, 2 * sizeof(int)),
false, MaxScale) / MaxFraction;
// Interpolate as a convex combination: min*(1 - fraction) + max*fraction stays within [min, max] for
// fraction in [0, 1], and no intermediate ever leaves the decimal range. The earlier midpoint form
// (mid ± half) overflowed on the full domain — it is symmetric, so max/2 rounds up and half = max/2 - min/2
Expand Down
11 changes: 9 additions & 2 deletions JustDummies/NullableExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ namespace JustDummies;
/// </summary>
public static class NullableExtensions {

/// <summary>
/// How many equiprobable outcomes the null-versus-value draw picks between — two, which is what makes
/// <c>null</c> come up about half the time. Shared with
/// <see cref="NullableReferenceExtensions.OrNull{T}" /> so the two siblings cannot drift to different rates.
/// </summary>
internal const int NullDrawOutcomes = 2;

/// <summary>
/// Derives a generator that yields <c>null</c> about half the time and, otherwise, a value drawn from
/// <paramref name="generator" /> — so a test exercises both the present and the absent case without pinning
Expand Down Expand Up @@ -39,7 +46,7 @@ public static class NullableExtensions {
return new DerivedAny<T?>(source, reproducible, () => {
RandomSource working = source ?? AmbientRandomSource.Instance;

return working.Current.Next(2) == 0 ? (T?)null : generator.Generate();
return working.Current.Next(NullDrawOutcomes) == 0 ? (T?)null : generator.Generate();
});
}

Expand Down Expand Up @@ -75,7 +82,7 @@ public static class NullableReferenceExtensions {
return new DerivedAny<T?>(source, reproducible, () => {
RandomSource working = source ?? AmbientRandomSource.Instance;

return working.Current.Next(2) == 0 ? (T?)null : generator.Generate();
return working.Current.Next(NullableExtensions.NullDrawOutcomes) == 0 ? (T?)null : generator.Generate();
});
}

Expand Down
2 changes: 1 addition & 1 deletion JustDummies/RandomSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,7 @@ internal static int NextInt32Inclusive(this SeededRandom random, int minInclusiv
internal static ulong NextUInt64(this SeededRandom random) {
if (random is null) { throw new ArgumentNullException(nameof(random)); }

byte[] bytes = new byte[8];
byte[] bytes = new byte[sizeof(ulong)];
random.NextBytes(bytes);

return BitConverter.ToUInt64(bytes, 0);
Expand Down
7 changes: 5 additions & 2 deletions JustDummies/RegexAlphabet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ internal static class RegexAlphabet {
internal const char MinPrintable = ' '; // 0x20
internal const char MaxPrintable = '~'; // 0x7E

/// <summary>How far an ASCII letter's two cases sit apart: <c>'a' - 'A'</c>, the single bit that tells them apart.</summary>
private const int AsciiCaseDistance = 'a' - 'A';

/// <summary>Every printable ASCII character — the universe negated classes and the dot draw from.</summary>
internal static readonly char[] Printable = Range(MinPrintable, MaxPrintable);

Expand Down Expand Up @@ -61,8 +64,8 @@ internal static char[] Negate(ISet<char> excluded) {
/// or class member matches either case.
/// </summary>
internal static IEnumerable<char> WithBothCases(char character) {
if (character is >= 'A' and <= 'Z') { return new[] { character, (char)(character + 32) }; }
if (character is >= 'a' and <= 'z') { return new[] { character, (char)(character - 32) }; }
if (character is >= 'A' and <= 'Z') { return new[] { character, (char)(character + AsciiCaseDistance) }; }
if (character is >= 'a' and <= 'z') { return new[] { character, (char)(character - AsciiCaseDistance) }; }

return new[] { character };
}
Expand Down
39 changes: 30 additions & 9 deletions JustDummies/RegexParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,27 @@ internal sealed class RegexParser {

private const int MaxGroupDepth = 256;

/// <summary>How many hexadecimal digits a <c>\xHH</c> escape spells.</summary>
private const int HexEscapeDigits = 2;

/// <summary>How many hexadecimal digits a <c>\uHHHH</c> escape spells.</summary>
private const int UnicodeEscapeDigits = 4;

/// <summary>How many octal digits may follow the first one in a <c>\0nn</c> escape.</summary>
private const int MaxOctalTailDigits = 2;

/// <summary>The base a <c>\x</c> or <c>\u</c> escape's digits accumulate in.</summary>
private const int HexBase = 16;

/// <summary>The base a <c>\0</c> escape's digits accumulate in.</summary>
private const int OctalBase = 8;

/// <summary>How many digits precede <c>'A'</c> in the hexadecimal alphabet, so <c>'A'</c> reads back as ten.</summary>
private const int HexLetterOffset = 10;

/// <summary>The control code <c>\cA</c> names — the alphabet's first letter maps to the first control character, not to the null one.</summary>
private const int FirstControlCode = 1;

internal static RegexNode Parse(string pattern, bool ignoreCase) {
if (pattern is null) { throw new ArgumentNullException(nameof(pattern)); }
RegexParser parser = new(pattern, ignoreCase);
Expand Down Expand Up @@ -74,7 +95,7 @@ private static bool IsHexDigit(char character) {
}

private static int HexValue(char character) {
return character <= '9' ? character - '0' : char.ToUpperInvariant(character) - 'A' + 10;
return character <= '9' ? character - '0' : char.ToUpperInvariant(character) - 'A' + HexLetterOffset;
}

#endregion
Expand Down Expand Up @@ -406,8 +427,8 @@ private RegexNode ParseEscape() {
case 'v': return Literal('\v');
case 'a': return Literal('\a');
case 'e': return Literal('\u001B');
case 'x': return Literal(ReadHexEscape(2));
case 'u': return Literal(ReadHexEscape(4));
case 'x': return Literal(ReadHexEscape(HexEscapeDigits));
case 'u': return Literal(ReadHexEscape(UnicodeEscapeDigits));
case 'c': return Literal(ReadControlEscape());
case '0': return Literal(ReadOctalTail(0));
case 'b': throw UnsupportedRegexException.OutsideRegularSubset(_pattern, "a word-boundary '\\b'", position);
Expand All @@ -432,8 +453,8 @@ private RegexNode ParseEscape() {
private char ReadHexEscape(int digits) {
int value = 0;
for (int i = 0; i < digits; i++) {
if (AtEnd || !IsHexDigit(Peek())) { throw Malformed($"a '\\{(digits == 2 ? 'x' : 'u')}' escape expects exactly {digits} hexadecimal digits"); }
value = value * 16 + HexValue(Next());
if (AtEnd || !IsHexDigit(Peek())) { throw Malformed($"a '\\{(digits == HexEscapeDigits ? 'x' : 'u')}' escape expects exactly {digits} hexadecimal digits"); }
value = value * HexBase + HexValue(Next());
}

return (char)value;
Expand All @@ -442,12 +463,12 @@ private char ReadHexEscape(int digits) {
private char ReadControlEscape() {
if (AtEnd || Peek() is not ((>= 'A' and <= 'Z') or (>= 'a' and <= 'z'))) { throw Malformed("a '\\c' escape expects a letter (\\cA through \\cZ)"); }

return (char)(char.ToUpperInvariant(Next()) - 'A' + 1);
return (char)(char.ToUpperInvariant(Next()) - 'A' + FirstControlCode);
}

private char ReadOctalTail(int firstDigit) {
int value = firstDigit;
for (int i = 0; i < 2 && !AtEnd && Peek() is >= '0' and <= '7'; i++) { value = value * 8 + (Next() - '0'); }
for (int i = 0; i < MaxOctalTailDigits && !AtEnd && Peek() is >= '0' and <= '7'; i++) { value = value * OctalBase + (Next() - '0'); }

return (char)value;
}
Expand Down Expand Up @@ -542,8 +563,8 @@ private char ReadClassChar() {
case 'a': return '\a';
case 'e': return '\u001B';
case 'b': return '\b'; // inside a class, \b is the backspace character, never a word boundary
case 'x': return ReadHexEscape(2);
case 'u': return ReadHexEscape(4);
case 'x': return ReadHexEscape(HexEscapeDigits);
case 'u': return ReadHexEscape(UnicodeEscapeDigits);
case 'c': return ReadControlEscape();
case '0': return ReadOctalTail(0);
default:
Expand Down
8 changes: 7 additions & 1 deletion JustDummies/Replay.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ namespace JustDummies;
[ValueObject]
internal sealed class Replay : IEquatable<Replay> {

/// <summary>
/// The odd prime the seed's hash is multiplied by before the guidance is folded in, so that the two fields
/// swapping values do not collide. Its exact value carries no meaning beyond being odd and prime.
/// </summary>
private const int HashMultiplier = 397;

#region Statics members declarations

/// <summary>
Expand Down Expand Up @@ -115,7 +121,7 @@ public override bool Equals(object? obj) {
/// <inheritdoc />
public override int GetHashCode() {
unchecked {
return (Seed * 397) ^ StringComparer.Ordinal.GetHashCode(Guidance);
return (Seed * HashMultiplier) ^ StringComparer.Ordinal.GetHashCode(Guidance);
}
}

Expand Down
Loading
Loading