4
\$\begingroup\$

I had to write a function which takes an angle in radians and precision N as input and returns a string representation of its magnitude in the format DDD°MM′SS.FFF″ where FFF is fraction of the second N decimal digits long.

As conversion of a floating-point binary number to a decimal string can be tricky, I've decided to not reinvent any wheels for generation of the .FFF part, and instead use some existing ones like QString::number. Here's the code I've written:

#include <QString>
#include <QStringList>
#include <cmath>
#include <cassert>

QString angleMagnitudeToString(double angleInRad, int precision)
{
    const auto angleInDeg = std::abs(angleInRad*180/M_PI);
    auto degrees = static_cast<int>(angleInDeg);
    auto minutes = static_cast<int>((angleInDeg-degrees)*60);
    const auto seconds = ((angleInDeg-degrees)*60-minutes)*60;

    const auto secStr = QString::number(seconds, 'f', precision);
    auto secStrIntAndFrac = secStr.split('.');
    if(precision==0)
        assert(secStrIntAndFrac.size()==1);
    else
        assert(secStrIntAndFrac.size()==2);

    // Rounding on converison to string may have resulted in 60 seconds, which should be carried to minutes
    if(secStrIntAndFrac[0]=="60")
    {
        secStrIntAndFrac[0]="0";
        ++minutes;
    }
    // Carry to minutes may have resulted in 60 minutes, which should be carried to degrees
    if(minutes==60)
    {
        minutes=0;
        ++degrees;
    }

    const auto intSecStr  = secStrIntAndFrac[0].rightJustified(2, '0');
    const auto fracSecStr = precision==0 ? "" : "."+secStrIntAndFrac[1];

    return QString::fromUtf8(u8"%1°%2′%3″")
                  .arg(degrees, 3, 10, QChar('0'))
                  .arg(minutes, 2, 10, QChar('0'))
                  .arg(intSecStr+fracSecStr);
}

The code seems kludgy due to the fact that I had to "manually" process the possible carry from the seconds to minutes to degrees, but I couldn't find any better way.

\$\endgroup\$
  • \$\begingroup\$ I'd love to see an implementation of the reverse function, which parses degree-minute-second strings and returns a value in radians. That would be especially useful to create a test of round-trip stability. \$\endgroup\$ – Toby Speight Nov 16 '17 at 11:44
3
\$\begingroup\$

Nice code - easy to read and well laid out. No surprises in the code, apart from the need to clear up the carry from seconds to minutes etc.

Some small improvements:

  • Although M_PI is in POSIX, if you want to be properly portable, you'll want to define your own π, perhaps as 4*std::atan(1.);

  • You might help the compiler to hoist the value of 180/PI if you re-write the multiplication as angleInRad*(180/PI) or 180/PI*angleInRad.

  • There's no checking that the degrees value is in range of int when casting.

  • It might make more sense to represent the angle in seconds or minutes rather than degrees (compromising the range in exchange for easier handling of the modular arithmetic).

My version

#include <QString>

#include <climits>
#include <cmath>

QString radians_to_dms(double radians, int precision)
{
    static const double PI = 4*std::atan(1.);

    if (std::abs(radians) > ULONG_MAX*PI/180/60)
        // out of range
        return "---";

    const auto total_minutes = std::abs(radians*(60*180/PI));
    auto whole_minutes = static_cast<int>(total_minutes);
    auto seconds = 60 * (total_minutes - whole_minutes);

    auto secStr = QString::number(seconds, 'f', precision);
    if (secStr.startsWith("60")) {
        // it rounds up - carry to minutes
        secStr = QString::number(0.0, 'f', precision);
        whole_minutes += 1;
    } else if (secStr.startsWith('-')) {
        // negative zero - shouldn't happen
        secStr = QString::number(0.0, 'f', precision);
    }
    if (secStr.size() < 2 || secStr[1] == '.')
        // zero-fill
        secStr.prepend('0');

    return QString::fromUtf8(u8"%1%2°%3′%4″")
        .arg(radians < 0 ? "-" : "")
        .arg(whole_minutes / 60, 3, 10, QChar('0'))
        .arg(whole_minutes % 60, 2, 10, QChar('0'))
        .arg(secStr);
}
#include <QDebug>
int main()
{
    for (double a: { -0.0, -3.1415926535897932384, 1.0,
                     59.999996/3600*4*std::atan(1.)/180,
                     1. * ULONG_MAX})
        qDebug() << a << " = " << qPrintable(radians_to_dms(a, 5));

    const double step = .000142857;
    for (double a = 0;  a < 10*step;  a+= step)
        qDebug() << a << " = " << qPrintable(radians_to_dms(a, 5));
}

I get reasonable output from the test cases:

0  =  000°00′00.00000″
-3.14159  =  -180°00′00.00000″
1  =  057°17′44.80625″
0.000290888  =  000°01′00.00000″
1.84467e+19  =  ---
0  =  000°00′00.00000″
0.000142857  =  000°00′29.46637″
0.000285714  =  000°00′58.93274″
0.000428571  =  000°01′28.39911″
0.000571428  =  000°01′57.86549″
0.000714285  =  000°02′27.33186″
0.000857142  =  000°02′56.79823″
0.000999999  =  000°03′26.26460″
0.00114286  =  000°03′55.73097″
0.00128571  =  000°04′25.19734″
0.00142857  =  000°04′54.66371″

I'm not sure whether it's much improvement over the original, but I hope it gives you some ideas.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.