diff --git a/xFunc.Maths/Analyzers/Simplifier.cs b/xFunc.Maths/Analyzers/Simplifier.cs index 8abe1c174..faa0a8c1a 100644 --- a/xFunc.Maths/Analyzers/Simplifier.cs +++ b/xFunc.Maths/Analyzers/Simplifier.cs @@ -26,7 +26,7 @@ private bool IsChanged(UnaryExpression old, IExpression argument) private bool IsChanged(BinaryExpression old, IExpression left, IExpression right) => old.Left != left || old.Right != right; - private IExpression AnalyzeUnaryArgument([NotNull] UnaryExpression exp) + private IExpression AnalyzeUnaryArgument(UnaryExpression exp) { if (exp is null) ArgNull(ExceptionArgument.exp); @@ -34,7 +34,7 @@ private IExpression AnalyzeUnaryArgument([NotNull] UnaryExpression exp) return exp.Argument.Analyze(this); } - private IExpression AnalyzeUnary([NotNull] UnaryExpression exp) + private IExpression AnalyzeUnary(UnaryExpression exp) { if (exp is null) ArgNull(ExceptionArgument.exp); @@ -47,7 +47,7 @@ private IExpression AnalyzeUnary([NotNull] UnaryExpression exp) return exp; } - private (IExpression Left, IExpression Right) AnalyzeBinaryArgument([NotNull] BinaryExpression exp) + private (IExpression Left, IExpression Right) AnalyzeBinaryArgument(BinaryExpression exp) { if (exp is null) ArgNull(ExceptionArgument.exp); @@ -58,7 +58,7 @@ private IExpression AnalyzeUnary([NotNull] UnaryExpression exp) return (left, right); } - private IExpression AnalyzeBinary([NotNull] BinaryExpression exp) + private IExpression AnalyzeBinary(BinaryExpression exp) { if (exp is null) ArgNull(ExceptionArgument.exp); @@ -71,7 +71,7 @@ private IExpression AnalyzeBinary([NotNull] BinaryExpression exp) return exp; } - private IExpression AnalyzeTrigonometric([NotNull] UnaryExpression exp) + private IExpression AnalyzeTrigonometric(UnaryExpression exp) where T : UnaryExpression { if (exp is null) @@ -87,7 +87,7 @@ private IExpression AnalyzeTrigonometric([NotNull] UnaryExpression exp) return exp; } - private IExpression AnalyzeDiffParams([NotNull] DifferentParametersExpression exp) + private IExpression AnalyzeDiffParams(DifferentParametersExpression exp) { if (exp is null) ArgNull(ExceptionArgument.exp); @@ -111,7 +111,7 @@ private IExpression AnalyzeDiffParams([NotNull] DifferentParametersExpression ex return exp; } - private IExpression AnalyzeVariableBinary([NotNull] VariableBinaryExpression exp) + private IExpression AnalyzeVariableBinary(VariableBinaryExpression exp) { if (exp is null) ArgNull(ExceptionArgument.exp); @@ -1090,7 +1090,20 @@ public override IExpression Analyze(Arctan exp) /// public override IExpression Analyze(Cos exp) - => AnalyzeTrigonometric(exp); + { + if (exp is null) + ArgNull(ExceptionArgument.exp); + + var argument = AnalyzeUnaryArgument(exp); + if (argument is Arccos { Argument: Number inverseNumber } arccos && + Arccos.Domain.IsInRange(inverseNumber.Value)) + return arccos.Argument; + + if (IsChanged(exp, argument)) + return exp.Clone(argument); + + return exp; + } /// public override IExpression Analyze(Cot exp) @@ -1098,15 +1111,54 @@ public override IExpression Analyze(Cot exp) /// public override IExpression Analyze(Csc exp) - => AnalyzeTrigonometric(exp); + { + if (exp is null) + ArgNull(ExceptionArgument.exp); + + var argument = AnalyzeUnaryArgument(exp); + if (argument is Arccsc { Argument: Number inverseNumber } arccsc && + Arccsc.Domain.IsInRange(inverseNumber.Value)) + return arccsc.Argument; + + if (IsChanged(exp, argument)) + return exp.Clone(argument); + + return exp; + } /// public override IExpression Analyze(Sec exp) - => AnalyzeTrigonometric(exp); + { + if (exp is null) + ArgNull(ExceptionArgument.exp); + + var argument = AnalyzeUnaryArgument(exp); + if (argument is Arcsec { Argument: Number inverseNumber } arcsec && + Arcsec.Domain.IsInRange(inverseNumber.Value)) + return arcsec.Argument; + + if (IsChanged(exp, argument)) + return exp.Clone(argument); + + return exp; + } /// public override IExpression Analyze(Sin exp) - => AnalyzeTrigonometric(exp); + { + if (exp is null) + ArgNull(ExceptionArgument.exp); + + var argument = AnalyzeUnaryArgument(exp); + if (argument is Arcsin { Argument: Number inverseNumber } arcsin && + Arcsin.Domain.IsInRange(inverseNumber.Value)) + return arcsin.Argument; + + if (IsChanged(exp, argument)) + return exp.Clone(argument); + + return exp; + } /// public override IExpression Analyze(Tan exp) diff --git a/xFunc.Maths/Expressions/Domains/Domain.cs b/xFunc.Maths/Expressions/Domains/Domain.cs new file mode 100644 index 000000000..8eb732c94 --- /dev/null +++ b/xFunc.Maths/Expressions/Domains/Domain.cs @@ -0,0 +1,67 @@ +// Copyright (c) Dmytro Kyshchenko. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace xFunc.Maths.Expressions.Domains; + +/// +/// Represents a domain of function. +/// +public readonly struct Domain : IEquatable +{ + private readonly DomainRange[] ranges; + + /// + /// Initializes a new instance of the struct. + /// + /// The array of ranges. + public Domain(DomainRange[] ranges) + => this.ranges = ranges; // TODO: validate? + + /// + /// Indicates whether domain is equal to the domain. + /// + /// The left domain. + /// The right domain. + /// true if the domain is equal to the domain; otherwise, false. + public static bool operator ==(Domain left, Domain right) + => left.Equals(right); + + /// + /// Indicates whether domain is not equal to the domain. + /// + /// The left domain. + /// The right domain. + /// true if the domain is not equal to the domain; otherwise, false. + public static bool operator !=(Domain left, Domain right) + => !left.Equals(right); + + /// + public bool Equals(Domain other) + => ranges.Equals(other.ranges); + + /// + public override bool Equals(object? obj) + => obj is Domain other && Equals(other); + + /// + public override int GetHashCode() + => ranges.GetHashCode(); + + /// + public override string ToString() + => string.Join(", ", ranges); + + /// + /// Determines whether the number is in the domain of the function. + /// + /// The number. + /// true, if the number is in the domain, otherwise, false. + public bool IsInRange(NumberValue value) + { + foreach (var range in ranges) + if (range.IsInRange(value)) + return true; + + return false; + } +} \ No newline at end of file diff --git a/xFunc.Maths/Expressions/Domains/DomainBuilder.cs b/xFunc.Maths/Expressions/Domains/DomainBuilder.cs new file mode 100644 index 000000000..83c3dac9c --- /dev/null +++ b/xFunc.Maths/Expressions/Domains/DomainBuilder.cs @@ -0,0 +1,48 @@ +// Copyright (c) Dmytro Kyshchenko. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace xFunc.Maths.Expressions.Domains; + +/// +/// The builder for . +/// +public class DomainBuilder +{ + private readonly DomainRange[] ranges; + private int index; + + /// + /// Initializes a new instance of the class. + /// + /// The amount of ranges to build. + public DomainBuilder(int rangeCount) + { + ranges = new DomainRange[rangeCount]; + index = 0; + } + + /// + /// Adds a range to the domain. + /// + /// The delegate to configure the domain range. + /// The builder. + /// The amount of added ranges exceeded the amount specified on the builder creation. + public DomainBuilder AddRange(Action configuration) + { + if (index >= ranges.Length) + throw new InvalidOperationException(); // TODO: + + var builder = new DomainRangeBuilder(); + configuration(builder); + ranges[index++] = builder.Build(); + + return this; + } + + /// + /// Builds the domain of the function. + /// + /// The domain of the function. + public Domain Build() + => new Domain(ranges); +} \ No newline at end of file diff --git a/xFunc.Maths/Expressions/Domains/DomainRange.cs b/xFunc.Maths/Expressions/Domains/DomainRange.cs new file mode 100644 index 000000000..6e5cf2aa8 --- /dev/null +++ b/xFunc.Maths/Expressions/Domains/DomainRange.cs @@ -0,0 +1,94 @@ +// Copyright (c) Dmytro Kyshchenko. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace xFunc.Maths.Expressions.Domains; + +/// +/// Represents a range of allowed values for the domain of function. +/// +public readonly struct DomainRange : IEquatable +{ + private readonly NumberValue start; + private readonly bool isStartInclusive; + private readonly NumberValue end; + private readonly bool isEndInclusive; + + /// + /// Initializes a new instance of the struct. + /// + /// The start of the range. + /// Determines whether the parameter is inclusive. + /// The end of the range. + /// Determines whether the parameter is inclusive. + public DomainRange(NumberValue start, bool isStartInclusive, NumberValue end, bool isEndInclusive) + { + if ((start.IsInfinity && isStartInclusive) || + (end.IsInfinity && isEndInclusive)) + throw new InvalidOperationException(); // TODO: + + this.start = start; + this.isStartInclusive = isStartInclusive; + this.end = end; + this.isEndInclusive = isEndInclusive; + } + + /// + /// Indicates whether range is equal to the range. + /// + /// The left range. + /// The right range. + /// true if the range is equal to the range; otherwise, false. + public static bool operator ==(DomainRange left, DomainRange right) + => left.Equals(right); + + /// + /// Indicates whether range is not equal to the range. + /// + /// The left range. + /// The right range. + /// true if the range is not equal to the range; otherwise, false. + public static bool operator !=(DomainRange left, DomainRange right) + => !left.Equals(right); + + /// + public bool Equals(DomainRange other) + => start.Equals(other.start) && + isStartInclusive == other.isStartInclusive && + end.Equals(other.end) && + isEndInclusive == other.isEndInclusive; + + /// + public override bool Equals(object? obj) + => obj is DomainRange other && Equals(other); + + /// + public override int GetHashCode() + => HashCode.Combine(start, isStartInclusive, end, isEndInclusive); + + /// + public override string ToString() + { + var leftBracket = isStartInclusive ? '[' : '('; + var rightBracket = isEndInclusive ? ']' : ')'; + + return $"{leftBracket}{start}; {end}{rightBracket}"; + } + + /// + /// Determines whether the number is in the range of domain. + /// + /// The number. + /// true, if the number is in the range of domain, otherwise, false. + public bool IsInRange(NumberValue value) + => CheckStart(value) && CheckEnd(value); + + private bool CheckStart(NumberValue value) + => isStartInclusive + ? start <= value + : start < value; + + private bool CheckEnd(NumberValue value) + => isEndInclusive + ? value <= end + : value < end; +} \ No newline at end of file diff --git a/xFunc.Maths/Expressions/Domains/DomainRangeBuilder.cs b/xFunc.Maths/Expressions/Domains/DomainRangeBuilder.cs new file mode 100644 index 000000000..1f4023bf1 --- /dev/null +++ b/xFunc.Maths/Expressions/Domains/DomainRangeBuilder.cs @@ -0,0 +1,74 @@ +// Copyright (c) Dmytro Kyshchenko. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace xFunc.Maths.Expressions.Domains; + +/// +/// The builder of . +/// +public class DomainRangeBuilder +{ + private NumberValue start; + private bool isStartInclusive; + private NumberValue end; + private bool isEndInclusive; + + /// + /// Configures the start of the range. + /// + /// The start of the range. + /// The builder. + public DomainRangeBuilder Start(NumberValue value) + { + start = value; + isStartInclusive = false; + + return this; + } + + /// + /// Configures the start (inclusive) of the range. + /// + /// The start of the range. + /// The builder. + public DomainRangeBuilder StartInclusive(NumberValue value) + { + start = value; + isStartInclusive = true; + + return this; + } + + /// + /// Configures the end of the range. + /// + /// The end of the range. + /// The builder. + public DomainRangeBuilder End(NumberValue value) + { + end = value; + isEndInclusive = false; + + return this; + } + + /// + /// Configures the end (inclusive) of the range. + /// + /// The end of the range. + /// The builder. + public DomainRangeBuilder EndInclusive(NumberValue value) + { + end = value; + isEndInclusive = true; + + return this; + } + + /// + /// Builds the domain range. + /// + /// The domain range. + public DomainRange Build() + => new DomainRange(start, isStartInclusive, end, isEndInclusive); +} \ No newline at end of file diff --git a/xFunc.Maths/Expressions/Trigonometric/Arccos.cs b/xFunc.Maths/Expressions/Trigonometric/Arccos.cs index ea35354c5..e734895b0 100644 --- a/xFunc.Maths/Expressions/Trigonometric/Arccos.cs +++ b/xFunc.Maths/Expressions/Trigonometric/Arccos.cs @@ -11,6 +11,11 @@ namespace xFunc.Maths.Expressions.Trigonometric; /// public class Arccos : InverseTrigonometricExpression { + private static readonly Lazy domain = new Lazy( + () => new DomainBuilder(1) + .AddRange(r => r.StartInclusive(-NumberValue.One).EndInclusive(NumberValue.One)) + .Build()); + /// /// Gets the lambda for the current expression. /// @@ -56,4 +61,9 @@ protected override TResult AnalyzeInternal( /// public override IExpression Clone(IExpression? argument = null) => new Arccos(argument ?? Argument); + + /// + /// Gets the domain of the function. + /// + public static Domain Domain => domain.Value; } \ No newline at end of file diff --git a/xFunc.Maths/Expressions/Trigonometric/Arccsc.cs b/xFunc.Maths/Expressions/Trigonometric/Arccsc.cs index 4d7e36c53..ea633ec9c 100644 --- a/xFunc.Maths/Expressions/Trigonometric/Arccsc.cs +++ b/xFunc.Maths/Expressions/Trigonometric/Arccsc.cs @@ -11,6 +11,12 @@ namespace xFunc.Maths.Expressions.Trigonometric; /// public class Arccsc : InverseTrigonometricExpression { + private static readonly Lazy domain = new Lazy( + () => new DomainBuilder(2) + .AddRange(r => r.Start(NumberValue.NegativeInfinity).EndInclusive(-NumberValue.One)) + .AddRange(r => r.StartInclusive(NumberValue.One).End(NumberValue.PositiveInfinity)) + .Build()); + /// /// Gets the lambda for the current expression. /// @@ -56,4 +62,9 @@ protected override TResult AnalyzeInternal( /// public override IExpression Clone(IExpression? argument = null) => new Arccsc(argument ?? Argument); + + /// + /// Gets the domain of the function. + /// + public static Domain Domain => domain.Value; } \ No newline at end of file diff --git a/xFunc.Maths/Expressions/Trigonometric/Arcsec.cs b/xFunc.Maths/Expressions/Trigonometric/Arcsec.cs index a9431620e..4c971ad01 100644 --- a/xFunc.Maths/Expressions/Trigonometric/Arcsec.cs +++ b/xFunc.Maths/Expressions/Trigonometric/Arcsec.cs @@ -11,6 +11,12 @@ namespace xFunc.Maths.Expressions.Trigonometric; /// public class Arcsec : InverseTrigonometricExpression { + private static readonly Lazy domain = new Lazy( + () => new DomainBuilder(2) + .AddRange(r => r.Start(NumberValue.NegativeInfinity).EndInclusive(-NumberValue.One)) + .AddRange(r => r.StartInclusive(NumberValue.One).End(NumberValue.PositiveInfinity)) + .Build()); + /// /// Gets the lambda for the current expression. /// @@ -56,4 +62,9 @@ protected override TResult AnalyzeInternal( /// public override IExpression Clone(IExpression? argument = null) => new Arcsec(argument ?? Argument); + + /// + /// Gets the domain of the function. + /// + public static Domain Domain => domain.Value; } \ No newline at end of file diff --git a/xFunc.Maths/Expressions/Trigonometric/Arcsin.cs b/xFunc.Maths/Expressions/Trigonometric/Arcsin.cs index f986f492b..3cd7f36e1 100644 --- a/xFunc.Maths/Expressions/Trigonometric/Arcsin.cs +++ b/xFunc.Maths/Expressions/Trigonometric/Arcsin.cs @@ -11,6 +11,11 @@ namespace xFunc.Maths.Expressions.Trigonometric; /// public class Arcsin : InverseTrigonometricExpression { + private static readonly Lazy domain = new Lazy( + () => new DomainBuilder(1) + .AddRange(r => r.StartInclusive(-NumberValue.One).EndInclusive(NumberValue.One)) + .Build()); + /// /// Gets the lambda for the current expression. /// @@ -56,4 +61,9 @@ protected override TResult AnalyzeInternal( /// public override IExpression Clone(IExpression? argument = null) => new Arcsin(argument ?? Argument); + + /// + /// Gets the domain of the function. + /// + public static Domain Domain => domain.Value; } \ No newline at end of file diff --git a/xFunc.Maths/xFunc.Maths.csproj b/xFunc.Maths/xFunc.Maths.csproj index 253af14f5..1f016b80f 100644 --- a/xFunc.Maths/xFunc.Maths.csproj +++ b/xFunc.Maths/xFunc.Maths.csproj @@ -48,6 +48,7 @@ https://github.com/sys27/xFunc/wiki/Breaking-Changes + diff --git a/xFunc.Tests/Analyzers/SimplifierTests/TrigonometricSimplifierTest.cs b/xFunc.Tests/Analyzers/SimplifierTests/TrigonometricSimplifierTest.cs index 56dc379bf..c4138e05e 100644 --- a/xFunc.Tests/Analyzers/SimplifierTests/TrigonometricSimplifierTest.cs +++ b/xFunc.Tests/Analyzers/SimplifierTests/TrigonometricSimplifierTest.cs @@ -12,12 +12,8 @@ public class TrigonometricSimplifierTest : BaseSimplifierTest [InlineData(typeof(Arccot), typeof(Cot))] [InlineData(typeof(Arcsec), typeof(Sec))] [InlineData(typeof(Arccsc), typeof(Csc))] - [InlineData(typeof(Sin), typeof(Arcsin))] - [InlineData(typeof(Cos), typeof(Arccos))] [InlineData(typeof(Tan), typeof(Arctan))] [InlineData(typeof(Cot), typeof(Arccot))] - [InlineData(typeof(Sec), typeof(Arcsec))] - [InlineData(typeof(Csc), typeof(Arccsc))] [InlineData(typeof(Arsinh), typeof(Sinh))] [InlineData(typeof(Arcosh), typeof(Cosh))] [InlineData(typeof(Artanh), typeof(Tanh))] @@ -30,7 +26,7 @@ public class TrigonometricSimplifierTest : BaseSimplifierTest [InlineData(typeof(Coth), typeof(Arcoth))] [InlineData(typeof(Sech), typeof(Arsech))] [InlineData(typeof(Csch), typeof(Arcsch))] - public void ReserseFunctionsTest(Type outer, Type inner) + public void InverseFunctionsTest(Type outer, Type inner) { var innerFunction = Create(inner, Variable.X); var outerFunction = Create(outer, innerFunction); @@ -39,6 +35,38 @@ public void ReserseFunctionsTest(Type outer, Type inner) SimplifyTest(outerFunction, expected); } + public static IEnumerable GetInverseDomainTestData() + { + yield return new object[] { new Sin(new Arcsin(Variable.X)), new Sin(new Arcsin(Variable.X)) }; + + yield return new object[] { new Sin(new Arcsin(Number.One)), Number.One }; + + yield return new object[] { new Sin(new Arcsin(Number.Two)), new Sin(new Arcsin(Number.Two)) }; + + yield return new object[] { new Cos(new Arccos(Variable.X)), new Cos(new Arccos(Variable.X)) }; + + yield return new object[] { new Cos(new Arccos(Number.One)), Number.One }; + + yield return new object[] { new Cos(new Arccos(Number.Two)), new Cos(new Arccos(Number.Two)) }; + + yield return new object[] { new Sec(new Arcsec(Variable.X)), new Sec(new Arcsec(Variable.X)) }; + + yield return new object[] { new Sec(new Arcsec(Number.Two)), Number.Two }; + + yield return new object[] { new Sec(new Arcsec(Number.Zero)), new Sec(new Arcsec(Number.Zero)) }; + + yield return new object[] { new Csc(new Arccsc(Variable.X)), new Csc(new Arccsc(Variable.X)) }; + + yield return new object[] { new Csc(new Arccsc(Number.Two)), Number.Two }; + + yield return new object[] { new Csc(new Arccsc(Number.Zero)), new Csc(new Arccsc(Number.Zero)) }; + } + + [Theory] + [MemberData(nameof(GetInverseDomainTestData))] + public void InverseDomainTest(IExpression exp, IExpression expected) + => SimplifyTest(exp, expected); + [Theory] [InlineData(typeof(Arcsin))] [InlineData(typeof(Arccos))] diff --git a/xFunc.ruleset b/xFunc.ruleset index c7c0cd415..db0c6dd69 100644 --- a/xFunc.ruleset +++ b/xFunc.ruleset @@ -9,6 +9,7 @@ +