What you have in mind is called discriminated union and is not available as language feature in C#.
I would use a hybrid approach where a fraction would contain nominator and denominator as int
plus prime factor lists as read-only properties. You could initialize the fraction with either ints or prime factor lists through two constructors. The missing entries would be calculated lazily to minimize the calculation overhead.
Making the properties read-only increases the robustness of the code. Especially if you are using structs. See Mutating readonly structs (Eric Lippert's blog: Fabulous adventures in coding). But note that the struct is still mutable because of the lazy evaluation.
public struct Fraction
{
public Fraction(int nominator, int denominator)
{
_nominator = nominator;
_denominator = denominator;
_nominatorPrimeFactors = null;
_denominatorPrimeFactors = null;
}
public Fraction(IList<int> nominatorPrimeFactors, IList<int> denominatorPrimeFactors)
{
if (nominatorPrimeFactors == null || nominatorPrimeFactors.Count == 0) {
throw new ArgumentNullException(
$"{nameof(nominatorPrimeFactors)} must be a non-null, non-empty list");
}
if (denominatorPrimeFactors == null || denominatorPrimeFactors.Count == 0) {
throw new ArgumentNullException(
$"{nameof(denominatorPrimeFactors)} must be a non-null, non-empty list");
}
_nominator = null;
_denominator = null;
_nominatorPrimeFactors = nominatorPrimeFactors;
_denominatorPrimeFactors = denominatorPrimeFactors;
}
private int? _nominator;
public int Nominator
{
get {
if (_nominator == null) {
_nominator = _nominatorPrimeFactors.Aggregate(1, (x, y) => x * y);
}
return _nominator.Value;
}
}
private int? _denominator;
public int Denominator
{
get {
if (_denominator == null) {
_denominator = _denominatorPrimeFactors.Aggregate(1, (x, y) => x * y);
}
return _denominator.Value;
}
}
private IList<int> _nominatorPrimeFactors;
public IList<int> NominatorPrimeFactors
{
get {
if (_nominatorPrimeFactors == null) {
_nominatorPrimeFactors = Factorize(Nominator);
}
return _nominatorPrimeFactors;
}
}
private IList<int> _denominatorPrimeFactors;
public IList<int> DenominatorPrimeFactors
{
get {
if (_denominatorPrimeFactors == null) {
_denominatorPrimeFactors = Factorize(Denominator);
}
return _denominatorPrimeFactors;
}
}
private static List<int> Factorize(int number)
{
var result = new List<int>();
while (number % 2 == 0) {
result.Add(2);
number /= 2;
}
int factor = 3;
while (factor * factor <= number) {
if (number % factor == 0) {
result.Add(factor);
number /= factor;
} else {
factor += 2;
}
}
if (number > 1) result.Add(number);
return result;
}
public override string ToString()
{
if (_nominatorPrimeFactors == null && _denominatorPrimeFactors == null) {
return $"{_nominator}/{_denominator}";
}
string npf = ListToString(_nominatorPrimeFactors);
string dpf = ListToString(_denominatorPrimeFactors);
if (_nominator == null && _denominator == null) {
return $"({npf}) / ({dpf})";
}
return $"{_nominator}/{_denominator}, ({npf}) / ({dpf})";
static string ListToString(IList<int> primeFactors)
{
if (primeFactors == null) {
return null;
}
return String.Join(" * ", primeFactors.Select(i => i.ToString()));
}
}
}
Note that declaring the prime factor lists a IList<int>
allows you to initialize the fraction with either int[]
or List<int>
.
But it is worth considering whether the prime factors really need to be stored. Isn't it enough to calculate them when required by some calculation?