C++ with Qt - Conversion of angle in radians to DDD°MM′SS.FFF″ string

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=="60")
{
secStrIntAndFrac="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.rightJustified(2, '0');
const auto fracSecStr = precision==0 ? "" : "."+secStrIntAndFrac;

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.

• 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. – Toby Speight Nov 16 '17 at 11:44

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>

{
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 == '.')
// 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.