diff --git a/cs/HomeExercises/NumberValidator.cs b/cs/HomeExercises/NumberValidator.cs new file mode 100644 index 00000000..7ca0651a --- /dev/null +++ b/cs/HomeExercises/NumberValidator.cs @@ -0,0 +1,55 @@ +using System; +using System.Text.RegularExpressions; +using JetBrains.Annotations; + +namespace HomeExercises +{ + public class NumberValidator + { + private readonly Regex numberRegex; + private readonly bool onlyPositive; + private readonly int precision; + private readonly int scale; + + public NumberValidator(int precision, int scale = 0, bool onlyPositive = false) + { + this.precision = precision; + this.scale = scale; + this.onlyPositive = onlyPositive; + if (precision <= 0) + throw new ArgumentException("precision must be a positive number"); + if (scale < 0 || scale >= precision) + throw new ArgumentException("precision must be a non-negative number less or equal than precision"); + numberRegex = new Regex(@"^([+-]?)(\d+)([.,](\d+))?$", RegexOptions.IgnoreCase); + } + + [Pure] + public bool IsValidNumber(string value) + { + // Проверяем соответствие входного значения формату N(m,k), в соответствии с правилом, + // описанным в Формате описи документов, направляемых в налоговый орган в электронном виде по телекоммуникационным каналам связи: + // Формат числового значения указывается в виде N(m.к), где m – максимальное количество знаков в числе, включая знак (для отрицательного числа), + // целую и дробную часть числа без разделяющей десятичной точки, k – максимальное число знаков дробной части числа. + // Если число знаков дробной части числа равно 0 (т.е. число целое), то формат числового значения имеет вид N(m). + + if (string.IsNullOrEmpty(value)) + return false; + + var match = numberRegex.Match(value); + if (!match.Success) + return false; + + // Знак и целая часть + var intPart = match.Groups[1].Value.Length + match.Groups[2].Value.Length; + // Дробная часть + var fracPart = match.Groups[4].Value.Length; + + if (intPart + fracPart > precision || fracPart > scale) + return false; + + if (onlyPositive && match.Groups[1].Value == "-") + return false; + return true; + } + } +} diff --git a/cs/HomeExercises/NumberValidatorTests.cs b/cs/HomeExercises/NumberValidatorTests.cs deleted file mode 100644 index a2878113..00000000 --- a/cs/HomeExercises/NumberValidatorTests.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System; -using System.Text.RegularExpressions; -using FluentAssertions; -using NUnit.Framework; - -namespace HomeExercises -{ - public class NumberValidatorTests - { - [Test] - public void Test() - { - Assert.Throws(() => new NumberValidator(-1, 2, true)); - Assert.DoesNotThrow(() => new NumberValidator(1, 0, true)); - Assert.Throws(() => new NumberValidator(-1, 2, false)); - Assert.DoesNotThrow(() => new NumberValidator(1, 0, true)); - - Assert.IsTrue(new NumberValidator(17, 2, true).IsValidNumber("0.0")); - Assert.IsTrue(new NumberValidator(17, 2, true).IsValidNumber("0")); - Assert.IsTrue(new NumberValidator(17, 2, true).IsValidNumber("0.0")); - Assert.IsFalse(new NumberValidator(3, 2, true).IsValidNumber("00.00")); - Assert.IsFalse(new NumberValidator(3, 2, true).IsValidNumber("-0.00")); - Assert.IsTrue(new NumberValidator(17, 2, true).IsValidNumber("0.0")); - Assert.IsFalse(new NumberValidator(3, 2, true).IsValidNumber("+0.00")); - Assert.IsTrue(new NumberValidator(4, 2, true).IsValidNumber("+1.23")); - Assert.IsFalse(new NumberValidator(3, 2, true).IsValidNumber("+1.23")); - Assert.IsFalse(new NumberValidator(17, 2, true).IsValidNumber("0.000")); - Assert.IsFalse(new NumberValidator(3, 2, true).IsValidNumber("-1.23")); - Assert.IsFalse(new NumberValidator(3, 2, true).IsValidNumber("a.sd")); - } - } - - public class NumberValidator - { - private readonly Regex numberRegex; - private readonly bool onlyPositive; - private readonly int precision; - private readonly int scale; - - public NumberValidator(int precision, int scale = 0, bool onlyPositive = false) - { - this.precision = precision; - this.scale = scale; - this.onlyPositive = onlyPositive; - if (precision <= 0) - throw new ArgumentException("precision must be a positive number"); - if (scale < 0 || scale >= precision) - throw new ArgumentException("precision must be a non-negative number less or equal than precision"); - numberRegex = new Regex(@"^([+-]?)(\d+)([.,](\d+))?$", RegexOptions.IgnoreCase); - } - - public bool IsValidNumber(string value) - { - // Проверяем соответствие входного значения формату N(m,k), в соответствии с правилом, - // описанным в Формате описи документов, направляемых в налоговый орган в электронном виде по телекоммуникационным каналам связи: - // Формат числового значения указывается в виде N(m.к), где m – максимальное количество знаков в числе, включая знак (для отрицательного числа), - // целую и дробную часть числа без разделяющей десятичной точки, k – максимальное число знаков дробной части числа. - // Если число знаков дробной части числа равно 0 (т.е. число целое), то формат числового значения имеет вид N(m). - - if (string.IsNullOrEmpty(value)) - return false; - - var match = numberRegex.Match(value); - if (!match.Success) - return false; - - // Знак и целая часть - var intPart = match.Groups[1].Value.Length + match.Groups[2].Value.Length; - // Дробная часть - var fracPart = match.Groups[4].Value.Length; - - if (intPart + fracPart > precision || fracPart > scale) - return false; - - if (onlyPositive && match.Groups[1].Value == "-") - return false; - return true; - } - } -} \ No newline at end of file diff --git a/cs/HomeExercises/ObjectComparison.cs b/cs/HomeExercises/ObjectComparison.cs deleted file mode 100644 index 44d9aed4..00000000 --- a/cs/HomeExercises/ObjectComparison.cs +++ /dev/null @@ -1,83 +0,0 @@ -using FluentAssertions; -using NUnit.Framework; - -namespace HomeExercises -{ - public class ObjectComparison - { - [Test] - [Description("Проверка текущего царя")] - [Category("ToRefactor")] - public void CheckCurrentTsar() - { - var actualTsar = TsarRegistry.GetCurrentTsar(); - - var expectedTsar = new Person("Ivan IV The Terrible", 54, 170, 70, - new Person("Vasili III of Russia", 28, 170, 60, null)); - - // Перепишите код на использование Fluent Assertions. - Assert.AreEqual(actualTsar.Name, expectedTsar.Name); - Assert.AreEqual(actualTsar.Age, expectedTsar.Age); - Assert.AreEqual(actualTsar.Height, expectedTsar.Height); - Assert.AreEqual(actualTsar.Weight, expectedTsar.Weight); - - Assert.AreEqual(expectedTsar.Parent!.Name, actualTsar.Parent!.Name); - Assert.AreEqual(expectedTsar.Parent.Age, actualTsar.Parent.Age); - Assert.AreEqual(expectedTsar.Parent.Height, actualTsar.Parent.Height); - Assert.AreEqual(expectedTsar.Parent.Parent, actualTsar.Parent.Parent); - } - - [Test] - [Description("Альтернативное решение. Какие у него недостатки?")] - public void CheckCurrentTsar_WithCustomEquality() - { - var actualTsar = TsarRegistry.GetCurrentTsar(); - var expectedTsar = new Person("Ivan IV The Terrible", 54, 170, 70, - new Person("Vasili III of Russia", 28, 170, 60, null)); - - // Какие недостатки у такого подхода? - Assert.True(AreEqual(actualTsar, expectedTsar)); - } - - private bool AreEqual(Person? actual, Person? expected) - { - if (actual == expected) return true; - if (actual == null || expected == null) return false; - return - actual.Name == expected.Name - && actual.Age == expected.Age - && actual.Height == expected.Height - && actual.Weight == expected.Weight - && AreEqual(actual.Parent, expected.Parent); - } - } - - public class TsarRegistry - { - public static Person GetCurrentTsar() - { - return new Person( - "Ivan IV The Terrible", 54, 170, 70, - new Person("Vasili III of Russia", 28, 170, 60, null)); - } - } - - public class Person - { - public static int IdCounter = 0; - public int Age, Height, Weight; - public string Name; - public Person? Parent; - public int Id; - - public Person(string name, int age, int height, int weight, Person? parent) - { - Id = IdCounter++; - Name = name; - Age = age; - Height = height; - Weight = weight; - Parent = parent; - } - } -} \ No newline at end of file diff --git a/cs/HomeExercises/Person.cs b/cs/HomeExercises/Person.cs new file mode 100644 index 00000000..144bc226 --- /dev/null +++ b/cs/HomeExercises/Person.cs @@ -0,0 +1,23 @@ +using System; + +namespace HomeExercises +{ + public class Person + { + public static int IdCounter = 0; + public int Age, Height, Weight; + public string Name; + public Person? Parent; + public int Id; + + public Person(string name, int age, int height, int weight, Person? parent) + { + Id = IdCounter++; + Name = name; + Age = age; + Height = height; + Weight = weight; + Parent = parent; + } + } +} \ No newline at end of file diff --git a/cs/HomeExercises/Tests/NumberValidatorTests.cs b/cs/HomeExercises/Tests/NumberValidatorTests.cs new file mode 100644 index 00000000..1f5a506c --- /dev/null +++ b/cs/HomeExercises/Tests/NumberValidatorTests.cs @@ -0,0 +1,72 @@ +using NUnit.Framework; +using System; + +namespace HomeExercises.Tests +{ + [TestFixture(TestOf = typeof(NumberValidator))] + public class NumberValidatorTests + { + [Test] + public void NumberValidatorCtor_WhenPassValidArguments_ShouldNotThrows() => + Assert.DoesNotThrow(() => new NumberValidator(1, 0, true)); + + private static TestCaseData[] ArgumentExceptionTestCases = + { + new TestCaseData(-1, 2, true).SetName("NegativePrecision"), + new TestCaseData(0, 2, true).SetName("PrecisionEqualToZero"), + new TestCaseData(1, -2, true).SetName("NegativeScale"), + new TestCaseData(2, 2, true).SetName("PrecisionIsEqualToTheScale"), + new TestCaseData(2, 3, true).SetName("ScaleIsMorePrecision") + }; + + [TestCaseSource(nameof(ArgumentExceptionTestCases))] + public void NumberValidatorCtor_WhenPassInvalidArguments_ShouldThrowArgumentException(int precision, int scale, bool onlyPositive) => + Assert.Throws(() => new NumberValidator(precision, scale, onlyPositive)); + + private static TestCaseData[] InvalidArgumentTestCases = + { + new TestCaseData(3,2,true,"a.sd").Returns(false).SetName("LettersInsteadOfNumber"), + new TestCaseData(3,2,true,"2.!").Returns(false).SetName("SymbolsInsteadOfNumber"), + new TestCaseData(3,2,true,null!).Returns(false).SetName("PassNumberIsNull"), + new TestCaseData(3,2,true,"").Returns(false).SetName("PassNumberIsEmpty"), + new TestCaseData(3,2,true," ").Returns(false).SetName("OnlySpaceInNumber"), + new TestCaseData(3,2,true," 2").Returns(false).SetName("MultipleSpacesAndOneDigitInNumber"), + new TestCaseData(3,2,true,"2,.3").Returns(false).SetName("TwoSeparatorsArePassed"), + new TestCaseData(3,2,true,".").Returns(false).SetName("OnlySeparatorArePassed"), + new TestCaseData(3,2,true,"2 3").Returns(false).SetName("SeparatedBySpace"), + new TestCaseData(3,2,true,"-0.00").Returns(false).SetName("IntPartWithNegativeSignMoreThanPrecision"), + new TestCaseData(3,2,true,"+1.23").Returns(false).SetName("IntPartWithPositiveSignMoreThanPrecision"), + new TestCaseData(3,2,true,"0.000").Returns(false).SetName("FractionalPartMoreThanScale"), + new TestCaseData(3,2,true,"2%3").Returns(false).SetName("PercentSignInTheFormOfSeparator"), + new TestCaseData(3,2,true,"2$").Returns(false).SetName("AmpersandInTheFormOfSeparator"), + new TestCaseData(3,2,true,"#").Returns(false).SetName("OctothorpeInTheFormOfNumber"), + new TestCaseData(3,2,true,"2@3").Returns(false).SetName("CommercialAtSymbolInTheFormOfSeparator"), + new TestCaseData(3,2,true,"(2.3)").Returns(false).SetName("NumberInParentheses"), + new TestCaseData(3,2,true,"2;3").Returns(false).SetName("SemicolonInTheFormOfSeparator"), + new TestCaseData(3,2,true,"2/r").Returns(false).SetName("CarriageReturnInNumber"), + new TestCaseData(3,2,true,"/n3").Returns(false).SetName("NewLineInNumber"), + new TestCaseData(3,2,true,"/t3.4").Returns(false).SetName("TabInNumber"), + new TestCaseData(3,2,true,"3.4/b").Returns(false).SetName("BackSpaceInNumber"), + new TestCaseData(3,2,true,"3.47e+10").Returns(false).SetName("NumberInExponentialForm"), + new TestCaseData(3,2,true,"10^3").Returns(false).SetName("NumberInAPower"), + new TestCaseData(3,2,true,"11101010").Returns(false).SetName("BinaryNumberSystem"), + new TestCaseData(3,2,true,"0xEA").Returns(false).SetName("HexadecimalNumberSystem"), + }; + + private static TestCaseData[] ValidArgumentTestCases = + { + new TestCaseData(3,2,true,"2,3").Returns(true).SetName("CharactersAreSeparatedByComma"), + new TestCaseData(3,2,true,"0").Returns(true).SetName("FractionalPartIsMissing"), + new TestCaseData(3,2,true,"0.0").Returns(true).SetName("NumberIsValid"), + new TestCaseData(19,2,true,"9223372036854775807").Returns(true).SetName("LargeIntPartInNumber"), + new TestCaseData(27,25,true,"3.1415926535897932384626433").Returns(true).SetName("LargeFracPartInNumber"), + new TestCaseData(45,25,true,"9223372036854775807.1415926535897932384626433").Returns(true).SetName("LargeNumber") + }; + + [TestOf(nameof(NumberValidator.IsValidNumber))] + [TestCaseSource(nameof(InvalidArgumentTestCases))] + [TestCaseSource(nameof(ValidArgumentTestCases)), Repeat(2)] + public bool NumberValidation_ShouldBeCorrect(int precision, int scale, bool onlyPositive, string number) => + new NumberValidator(precision, scale, onlyPositive).IsValidNumber(number); + } +} diff --git a/cs/HomeExercises/Tests/ObjectComparisonTests.cs b/cs/HomeExercises/Tests/ObjectComparisonTests.cs new file mode 100644 index 00000000..48dc1a78 --- /dev/null +++ b/cs/HomeExercises/Tests/ObjectComparisonTests.cs @@ -0,0 +1,57 @@ +using FluentAssertions; +using NUnit.Framework; +using System; + +namespace HomeExercises.Tests +{ + [TestFixture(TestOf = typeof(TsarRegistry))] + public class ObjectComparisonTests + { + [Test] + [Description("Проверка текущего царя")] + [Category("ToRefactor")] + public void CheckCurrentTsar() + { + var actualTsar = TsarRegistry.GetCurrentTsar(); + + var expectedTsar = new Person("Ivan IV The Terrible", 54, 170, 70, + new Person("Vasili III of Russia", 28, 170, 60, null)); + + actualTsar.Should() + .BeEquivalentTo(expectedTsar, x => x + .Excluding(x => x.Id) + .Excluding(x => x.Parent!.Id)); + } + + [Test] + [Description("Альтернативное решение. Какие у него недостатки?")] + public void CheckCurrentTsar_WithCustomEquality() + { + var actualTsar = TsarRegistry.GetCurrentTsar(); + var expectedTsar = new Person("Ivan IV The Terrible", 54, 170, 70, + new Person("Vasili III of Russia", 28, 170, 60, null)); + + // Какие недостатки у такого подхода? + // Данный подход делает класс труднорасширяемым, ведь при добавлении новых полей в класс Person + // нужно переписывать и метод сравнения для всех этих полей, так же новые поля cможет добавить + // другой разработчик, который не знает о таком методе сравнения, что приведет к новым, неотловоенным ошибкам - + // новые поля не будут сравниваться. + // В моем решении (CheckCurrentTsar), такой ошибки не возникнет и класс Person сможет без ошибок расширяться + // при условии, что сравниваться будут все поля, кроме Id (так же и их Parent), так как код написан не перебиранием + // всех полей для сравнения, а сравнением объекта в целом с исключением его Id. + Assert.True(AreEqual(actualTsar, expectedTsar)); + } + + private bool AreEqual(Person? actual, Person? expected) + { + if (actual == expected) return true; + if (actual == null || expected == null) return false; + return + actual.Name == expected.Name + && actual.Age == expected.Age + && actual.Height == expected.Height + && actual.Weight == expected.Weight + && AreEqual(actual.Parent, expected.Parent); + } + } +} diff --git a/cs/HomeExercises/TsarRegistry.cs b/cs/HomeExercises/TsarRegistry.cs new file mode 100644 index 00000000..42948c68 --- /dev/null +++ b/cs/HomeExercises/TsarRegistry.cs @@ -0,0 +1,14 @@ +using System; + +namespace HomeExercises +{ + public class TsarRegistry + { + public static Person GetCurrentTsar() + { + return new Person( + "Ivan IV The Terrible", 54, 170, 70, + new Person("Vasili III of Russia", 28, 170, 60, null)); + } + } +} \ No newline at end of file