Math.Round, double, dokładność do N miejsc po przecinku, MSTest

0

Trochę się zakręciłem w problemie i potrzebuje czyjegoś spojrzenia z zewnątrz, będę wdzięczny za podpowiedzi.

Opis Problemu:
Pobieram z pliku tekstowego wartość czasu wyrażonego dziesiętnie np. '0,25', '7,67', '0,33333', '0,044'. Taka postać tekstowa tej wartości czasu, ma różną liczbę cyfr po przecinku, może mieć dokładność od 0 do 5 miejsc po przecinku, w zależności od tego co to za czas przygotowania/realizacji/nakładania/itp.
Ale jest tak że np. czas realizacji w całym systemie może być reprezentowany przez 'double' z dokładnością do 5ciu miejsc po przecinku a w niektórych przypadkach w pliku tekstowym z którego importuje dane, ma dokładność 2 miejsc po przecinku, i to jest ok, czasami tak bywa np. 15min to '0,25'. (Nie mam wpływu na plik tekstowy i nie uzyskam tam wartości '0,25000' dla 15minut.)

Następnie konwertuję taki czas na 'double' w obiekcie który czyta ten plik i w tym obiekcie buduję swój obiekt 'CzasScala' do którego konstruktora podaje już 'double' a nie 'string' (potrzebuje takiego obiektu do obliczeń, edycji i wyswietlania tego czasu w różnych formatach w programie).

    public class CzasScala : IComparable<CzasScala>
    {
        public int Dokladnosc_CzasDec_PoPrzecinku { get; private set; }
       
        public CzasScala(double d, int dokladnoscPoPrzecinku)
        {
            Dokladnosc_CzasDec_PoPrzecinku = dokladnoscPoPrzecinku;
            UstawNaPodstawieDziesietnej(d);
        }
        public double GodzinDziesietnie
        {
            get { return _godzinDziesietnie; }
            set { UstawNaPodstawieDziesietnej(value); }
        }
        private double _godzinDziesietnie;

        public int SumaSekund
        {
            get { return _sumaSekund; }
            set { UstawNaPodstawieSumySekund(value); }
        }
        private int _sumaSekund;

        public TimeSpan CzasStandard
        {
            get { return _czasStandard; }
            set { UstawNaPodstawieDateTime(value); }
        }
        private TimeSpan _czasStandard;

        private void UstawNaPodstawieDziesietnej(double d)
        {
            _sumaSekund = WyliczSekundy(d);
            WyliczSkladoweZSumySekund(_sumaSekund);
        }

        private int WyliczSekundy(double d)
        {
            return (int)(d * 3600d);
        }
        private void WyliczSkladoweZSumySekund(int s)
        {
            _czasH = (_sumaSekund / 3600);
            _czasM = (_sumaSekund % 3600 / 60);
            _czasS = (_sumaSekund % 3600 % 60);

            _godzinDziesietnie = Math.Round((double)s / 3600d, Dokladnosc_CzasDec_PoPrzecinku);
            _czasStandard = new TimeSpan(_czasH, _czasM, _czasS);
        }

//pozostala czesc klasy

I teraz problem, jeżeli opakuje taki czas realizacji '0,044' w 'CzasScala' i przeprowadzę na nim jakieś obliczenia lub edycje do postaci np. '0,3784' to dalej w programie wyeksportuje go do pliku tekstowego w takiej właśnie formie '0,3784' i to jest ok, tak ma być. Ale jeżeli pobiorę czas realizacji w formie '0,044' i nic z nim nie zrobię w programie to chciałbym go wyeksportować w postaci identycznej jak pobrałem czyli '0,044' a teraz wg tego co robi mój program, nie ruszony czas zapisuje mi w formie '0,04389' a tak nie chce.

Wiem ze problem leży w metodzie 'WyliczSkladoweZSumySekund' i doszedłem w 'Consoli' jak uzyskać to co chce, po prostu ustawiając właściwość w klasie musze ją ustawiać z dokładnością taką z jaką przychodzi z tekstu, ale jedyny sposób jaki wymyśliłem żeby pobierać dokładność po przecinku wartości która przychodzi to Split(',') i pobranie długości ostatniego stringa z uzyskanej tablicy stringów - ale to mi się wydaje za grubo. Jest jakiś inny rozsądny sposób aby uzyskać efekt o jaki mi chodzi? Może cos zamiast 'Math.Round'

 static void Main(string[] args)
        {
            //odczytana, docelowaDokladnoscPoPrzecinku, sumaSekund, Gdzin, Minut,Sekund, CultureInfo
            //[DataRow(15.35  , 2, 55260, 15, 21, 0, "en-US")]
            //[DataRow(0.044  , 4, 158  , 0, 2, 38, "en-US")]
            //[DataRow(0.064  , 2, 230  , 0, 3, 50, "en-US")]
            //[DataRow(0.0645 , 2, 232  , 0, 3, 52, "en-US")]
            CultureInfo.CurrentCulture = new CultureInfo("en-US", false);

            double d = 0.044;

            int sekund = 158;
            int dokladnoscDocelowa = 5;


            double sekundDouble = (double)sekund;
            double tymczasowa = Math.Round(sekundDouble / 3600d, 3);   //tu na sztywno podaje dokladnosc 3 miejsc po przecinku (bo 0.044)
            double _godzinDziesietnie = Math.Round(tymczasowa, dokladnosc);

            Console.WriteLine($"Przypadek dla {d}");
            Console.WriteLine();
            Console.WriteLine($"Sekund = '{sekund}'\t\n" +
                $"dokladnosc: '{dokladnosc}' miejsc po przecinku, \t\n" +
                $"wartość uzyskana Double = '{_godzinDziesietnie}'");
            Console.ReadLine();
        }

Problem uwidocznił się w programie i dlatego żeby zacząć go rozwiązywać zacząłem tworzyć testy jednostkowe.

        // doubleH_IN, poPrzecinkuIN, SUMAsekundOUT, godzinOUT, minutOUT , sekundOUT, culture
        [DataTestMethod]
        [DataRow(3.5,       2,  12600,  3,  30, 0,  "en-US")]
        [DataRow(0.25,      2,  900,    0,  15, 0,  "en-US")]
        [DataRow(0.17,      4,  612,    0,  10, 12, "en-US")]
        [DataRow(15.35,     2,  55260,  15, 21, 0,  "en-US")]
        [DataRow(0.044,     5,  158,    0,  2,  38, "en-US")]
        [DataRow(0.064,     2,  230,    0,  3,  50, "en-US")]
        [DataRow(0.0645,    2,  232,    0,  3,  52, "en-US")]
        [DataRow(0.33333,   5,  1200,   0,  20, 0,  "en-US")]
        [DataRow(0.66666,   2,  2400,   0,  40, 0,  "en-US")]
        [DataRow(0.66,      1,  2376,   0,  39, 36, "en-US")]
        [DataRow(0.67,      1,  2412,   0,  40, 12, "en-US")]
        [DataRow(0.00001,   0,  0,      0,   0,  0, "en-US")]
        public void TestKonstruktora_DoubleInt_CultureEN_TestUstawianiaWszsytkichPol(double d, int i,
            int sumaSekundResult, int godzinResult, int minutResult, int sekundResult, string cultureInfo)
        {
            CultureInfo.CurrentCulture = new CultureInfo(cultureInfo, false);

            CzasScala czas = new CzasScala(d, i);
            TimeSpan tsResult = new TimeSpan(godzinResult, minutResult, sekundResult);

            Assert.AreEqual(sumaSekundResult, czas.SumaSekund, $"blad w 'SumaSekund' spodziewalem sie {sumaSekundResult} a otrzymalem {czas.SumaSekund}");
            Assert.AreEqual(d, czas.GodzinDziesietnie, $"blad w 'GodzinaDziesietnie' spodziewalem sie {d} a otrzymalem {czas.GodzinDziesietnie}");
            Assert.AreEqual(tsResult, czas.CzasStandard, $"blad w 'TimeSpan', spodziewalem sie {tsResult} a otrzymalem {czas.CzasStandard}");
        }
2

Napisz sobie klasę do ułamków zwykłych. Jak użyjesz w niej BigInteger, to masz praktycznie nieograniczoną precyzję i odporność na wewnętrzną reprezentację liczb zmiennoprzecinkowych w komputerach.

1

Zakładam, masz ugruntowane liczby zmiennoprzecinkowe (choćby na poziomie wikipedii), i wiesz, ze mają dwa problemy:

  • dokładności
  • reprezentacji
0

na typie decimal mam ten sam problem:

            decimal oczekiwana = 0.044m;
            decimal zmiennaD = 158;
            decimal wynik = zmiennaD / 3600m;
            decimal docelowaDoPieciuMiejscPoPrzecinku = Math.Round(wynik, dokladnoscDocelowa);

            Console.WriteLine($"oczekiwane {oczekiwana}, na wyjsciu '{docelowaDoPieciuMiejscPoPrzecinku}'");

wynik:

oczekiwane 0.044, na wyjsciu '0.04389'

0

Ale czemu oczekujesz że wyjdzie ci 0.044? Podziel sobie to na kalkulatorze zaokrąglij do 5 miejsc i zobaczysz że dostaniesz to samo

0

Kurczę, czasami trzeba wstać i się przejść.

Do konstruktora przekazuje 'double' sparsowany ze 'string'a', ale nie przepisuje go tylko przeliczam na sekundy i dopiero w metodzie 'WyliczSkladoweZSumySekund' przeliczam jeszcze raz to samo do 'double' ale już z inna dokładnością. Chciałem mieć za wszelka cenę jedna metodę.
Teraz dodałem przeciążoną metodę 'WyliczSkladoweZSumySekund(int s, double d)' i ona tylko przepisuję odczytany 'double' do właściwości, dzięki temu mam to co zostało przeczytane z tekstu i jeżeli nie ruszę tego w czasie życia obiektu 'CzasScala' to tak samo zostanie wyeksportowane do pliku.

        private void WyliczSkladoweZSumySekund(int s, double d)
        {
            _czasH = (_sumaSekund / 3600);
            _czasM = (_sumaSekund % 3600 / 60);
            _czasS = (_sumaSekund % 3600 % 60);

            _godzinDziesietnie = d;
            _czasStandard = new TimeSpan(_czasH, _czasM, _czasS);
        }

W końcu wszystkie testy przechodzą.

1 użytkowników online, w tym zalogowanych: 0, gości: 1