Hi!
Qbasic is great as a hobbyist programmable scientific calculator. My special interest is electronics so I write many little ditties to analyze circuitry. But, I get tired of the confusing numerical output format that is default in Qbasic. Some examples:
An output of electrical inductance might look like this: 0.01267983 Henrys. This value might be correct, but is difficult to read because it is in non-standard form. What we really want to see is this: "12.68 milli-Henrys". This form uses a standard scientific prefix and rounds the value off to the four most significant digits.
To be "readable", 0.0001267983 Henrys needs to be output as "126.8 micro-henrys". And, 2099999486 watts need to be output as "2.100 gigga-watts".
Note that the prefix "gigga" is a term coined by the noted physicist, Emmett Brown who disappeared one stormy night in late 1985, taking with him the secrets of his Flux Capacitor and setting back the development of time travel theory for many decades. Reports that Doctor Brown was killed by middle-eastern terrorists are unfounded and his disappearance has remained a mystery to this day.
Yea. Well anyway... The problem is formatting the output to print only what is important -- the rounded MSD's -- regardless of where the decimal point is. I haven't found any way to do it using basic's "USING". Anybody have any ideas?
I wrote a small function to do the prefixes:
[code]
DECLARE FUNCTION pformat$ (n!, units$)
'a little test stub:
start:
INPUT x!
CLS
PRINT x!; "watts = "; pformat$(x!, "watts")
GOTO start
FUNCTION pformat$ (n!, units$)
'Nine standard scientific prefixes.....
DIM prefix$(8)
prefix$(0) = "Tera-" '1e12 - trillions
prefix$(1) = "Giga-" '1e9 (or "gigga") - billions
prefix$(2) = "Mega-" '1e6 - millions
prefix$(3) = "Kilo-" '1e3 - thousands
prefix$(4) = " " 'units 1e0 - ones
prefix$(5) = "milli-" '1e-3 - thousandths
prefix$(6) = "micro-" '1e-6 - millionths
prefix$(7) = "nano-" '1e-9 - thousand-millionths
prefix$(8) = "pico-" '1e-12 - million-millionths
limit! = 1E+12
FOR i% = 0 TO 8
IF ABS(n!) > limit! THEN
pformat$ = STR$(n! / limit!) + " " + prefix$(i%) + units$
EXIT FOR
END IF
limit! = limit! / 1000!
NEXT i%
'crude error handling...
IF i% = 9 THEN
PRINT "input error"
STOP
END IF
END FUNCTION
[/code]
And a slightly more simple one that just uses common abbreviations for the prefixes as is common in science:
[code]
DECLARE FUNCTION pformat$ (n!, units$)
'just a test stub:
start:
INPUT x!
CLS
PRINT pformat$(x!, "watt"); " Flux Capacitor"
GOTO start
FUNCTION pformat$ (n!, units$)
'prefixes ...
' "T" - Tera - 1e12 - trillions
' "G" - giga - 1e9 - billions (or "gigga")
' "M" - mega - 1e6 - millions
' "K" - kilo - 1e3 - thousands
' " " - (no prefix) - 1e0 - ones
' "m" - milli - 1e-3 - thousandths
' "u" - micro - 1e-6 - millionths
' "n" - nano - 1e-9 - thousand-millionths
' "p" - pico - 1e-12 - million-millionths
CONST prefix$ = "TGMK munp"
limit! = 1E+12
FOR i% = 1 TO 9
IF ABS(n!) > limit! THEN
pformat$ = STR$(n! / limit!) + " " + MID$(prefix$, i%, 1) + units$
EXIT FOR
END IF
limit! = limit! / 1000!
NEXT i%
'some crude error handling...
IF i% = 10 THEN
PRINT "input error"
STOP
END IF
END FUNCTION
[/code]
But inputting 2099999486 as in the above "Gigga-watt" example, the output is 2.099999 Gwatts. How can I make it round of to four MSD's so the output is 2.100 Gwatts?
Ideas, suggestions, algorithms, code welcome! Thanks!
rg
Comments
Try this
num = 2.4563548#
PRINT num
num = INT((num + .00005) * 10000) / 10000
PRINT num
Adding .00005 will increase the fourth decimal place by one if the
fifth decimal place is 5 or larger.
Multiplying by 10000 puts the first 4 decimal places to the left of the decimal point.
Using 'INT ' removes the remaining decimal places.
Dividing by 10000 puts the first four decimal places back to the right
of the decimal point.
Hope this helps
Pappy
You learn something new everyday.
I love Back to the Future! Anyway, I tried to produce a solution for you. I tested it with the 2 examples you gave and it worked ok, but there could easily be some flaws in it.. This'll give you an idea on how to do it, atleast.
[code]
DECLARE FUNCTION xformat$ (iStr AS STRING, sDigits AS INTEGER)
DECLARE FUNCTION pformat$ (n!, units$)
'just a test stub:
start:
INPUT x!
CLS
PRINT xformat$(pformat$(x!, "watt"), 4); " Flux Capacitor"
GOTO start
FUNCTION pformat$ (n!, units$)
'prefixes ...
' "T" - Tera - 1e12 - trillions
' "G" - giga - 1e9 - billions (or "gigga")
' "M" - mega - 1e6 - millions
' "K" - kilo - 1e3 - thousands
' " " - (no prefix) - 1e0 - ones
' "m" - milli - 1e-3 - thousandths
' "u" - micro - 1e-6 - millionths
' "n" - nano - 1e-9 - thousand-millionths
' "p" - pico - 1e-12 - million-millionths
CONST prefix$ = "TGMK munp"
limit! = 1E+12
FOR i% = 1 TO 9
IF ABS(n!) > limit! THEN
pformat$ = STR$(n! / limit!) + " " + MID$(prefix$, i%, 1) + units$
EXIT FOR
END IF
limit! = limit! / 1000!
NEXT i%
'some crude error handling...
IF i% = 10 THEN
PRINT "input error"
STOP
END IF
END FUNCTION
FUNCTION xformat$ (iStr AS STRING, sDigits AS INTEGER)
DIM nStr AS LONG, xVal AS DOUBLE, xStr AS STRING, iiStr AS STRING
DIM zStr AS STRING
iiStr = RTRIM$(LTRIM$(iStr))
nStr = INSTR(iiStr, " ")
IF nStr = 0 THEN
zStr = ""
ELSE
zStr = LTRIM$(RIGHT$(iiStr, LEN(iiStr) - nStr))
END IF
nStr = INSTR(iiStr, ".")
xVal = INT(VAL(iiStr) * 10 ^ (sDigits - nStr + 1) + .5) / 10 ^ (sDigits - nStr + 1)
xStr = LTRIM$(STR$(xVal))
IF LEN(xStr) < sDigits AND INSTR(xStr, ".") = 0 THEN
xStr = xStr + STRING$(sDigits - LEN(xStr), "0")
ELSEIF LEN(xStr) < sDigits + 1 AND INSTR(xStr, ".") <> 0 THEN
xStr = xStr + STRING$(sDigits - LEN(xStr) + 1, "0")
END IF
xformat$ = xStr + " " + zStr
END FUNCTION
[/code]
And I don't know if you've run across this yet, but when you're making these microscopic calculations, you'll have much more exact results if you convert to integer (mili or whatever the case may be) before making the calculations, and then either display the data in converted form or convert back to it's original form if you need to.. QBasic, or computers in general, have a flaw when dealing with floating point numbers.. The problem is that they work on binary and we, in the world around us, work on base 10. Try this:
[code]
DIM y AS DOUBLE
CLS
y = 1
FOR x = 1 TO 22
y = y + .1
PRINT y
NEXT
[/code]
We, in the base 10 world, know that 1 + .1 + .1 + .1.. (22 times) = 3.2 exactly, but the computer says it equals 3.20000003278256. The reason for this is that there's no way to use .1 as a number in binary.. If you convert it to binary you end up with a repeating number after the decimal point (or "binary point".. whatever).. thus it has to be cut off somewhere, and that's why it starts producing errors. Now, in truth, 3.20000003278256 doesn't make any difference from 3.2, especially when you round to significant digits, but try it with y being a SINGLE instead of just a double. After just 22 loops it thinks 1 + .1 + .1.. (22 times) = 3.199999, which although is still 3.200 with significant digits, should point out that after many calculations it wouln't be hard for a flaw to creep in.. Especially when working with numbers like .00000001 instead of .1. That is why, if you're trying to get exact numbers with very small floating points, it is a good idea to convert to an integer expression, and then back. For example:
[code]
DIM y AS LONG
CLS
y = 10
DO
y = y + 1
PRINT y / 10
LOOP
[/code]
You can run the above code for days without being given any incorrect answers. The only time you'll have an error is when y reaches 2,147,483,648 and can no longer hold such a large number as a LONG. Of course in the real world, adding DOUBLES works fine, and of course that is the point of significant digits, is it not? To account for any errors that may have been made by uncontrollable factors while reaching the answer.. Of course, in reality you can probably go on using DOUBLES and be completely satisfied. I just thought you may want to know, since scientists can also sometimes be sticklers for exact answers, and if you loop a DOUBLE enough (A LOT) you could end up with a slightly incorrect answer.
Well, it worked on some, but not all the inputs. Although, in retrospect I think at least some of the problems were in my pformat() code. But as you suggested, it got me off onto another path that has led to a solution.
: And I don't know if you've run across this yet, but when you're making these microscopic calculations, you'll have much more exact results if you convert to integer (mili or whatever the case may be) before making the calculations, and then either display the data in converted form or convert back to it's original form if you need to.. QBasic, or computers in general, have a flaw when dealing with floating point numbers.. The problem is that they work on binary and we, in the world around us, work on base 10.
Yes! In fact I run into decimal/binaryfp/decimal conversion inaccuracies all the time. Most of the time it's not a problem in hobby-type electronics and using qbasic's single precision gives more than enough accuracy for real-world applications. It is one thing to calculate the value of a capacitor to 7 or 15 digits, but if you walk into the parts house and say "Gimme a 0.000000116573000000001 Farad capacitor", all you will get is a very strange look. What you really want is a ".12 microfarad" Most component parts values are accurate only to within 5 or 10 percent which translates to only 2 or 3 significant figures.
This is what I am looking for in my programs -- calculations with not-perfect, but reasonably accurate precision, and real-world formatted outputs.
Computer precision and very large or very small numbers lead to a situation where qbasic will switch to scientific notation for output or string conversion. It is hard to predict when the switch will happen. At times it was happening as soon as I converted the incoming double to a string, and at other times it would happen during the round-off. This was one major reason I was having so much trouble with the format routine. As for the input conversion, I finally accepted that it WILL happen, and prepared for both types of string conversion. Your "conversion to long integer" idea looks like it would do the job, but I went one step farther -- I just rounded by processing the string, character by character:
[code]
'this version is compatible with QB 4.5
DECLARE FUNCTION pformat$ (n AS DOUBLE, d AS INTEGER, u AS STRING)
CLS
DO
INPUT a#
PRINT TAB(10); a#, pformat(a#, 3, "parsecs")
LOOP
FUNCTION pformat$ (n AS DOUBLE, d AS INTEGER, u AS STRING)
'the unit prefixes: pico, nano, micro, milli, (none), Kilo, Mega, Giga, Tera
CONST prefix = "pnum KMGT"
DIM nstr AS STRING 'string representaion of the number
DIM sign AS STRING 'the sign of the number
DIM outv AS STRING 'output value
DIM outu AS STRING 'output units
DIM e AS INTEGER 'scientific notation exponent of 10
DIM i AS INTEGER 'general purpose counter
DIM t AS INTEGER 'temporary general purpose
nstr = STR$(n) 'get the string version of the number
nstr = RTRIM$(nstr) 'strip off the trailing blank
sign = LEFT$(nstr, 1) 'save the number's sign...
nstr = MID$(nstr, 2) '...then strip the sign off the number
'what's left might be either a traditional decimal number or
'a number in scientific notation...
t = INSTR(nstr, "D") 'is it in scientific notation??
IF t THEN 'yep! it's in scientific notation...
e = VAL(MID$(nstr, t + 1)) 'get the exponent...
nstr = LEFT$(nstr, t - 1) '...then remove it from the string
nstr = LEFT$(nstr, 1) + MID$(nstr, 3) 'remove the decimal point
ELSE 'nope! -- it's simply a decimal...
e = INSTR(nstr, ".") 'get the position of the dec point
IF e = 0 THEN 'if no dp then it is implied....
nstr = nstr + "." '...so we will just add our own dp
e = INSTR(nstr, ".") 'get the position of the dec point
END IF
nstr = LEFT$(nstr, e - 1) + MID$(nstr, e + 1) 'strip out the dp
e = e - 2 'adjust the dp to reflect the value of an exponent
END IF
' Whether QB handed us a simple decimal number or a number in
' scientific notation, we now have a string representation of the
' significant digits of the number; that if normalized by placing
' a radix immediately following the most significant digit, becomes
' the mantissa of the number, with the signed exponent in e, and
' the mantissa's sign character in sign.
nstr = nstr + STRING$(d, "0") 'add some trailing zeros to make nice
' strip off the leading zeros to leave only significant digits...
IF VAL(nstr) THEN '...but not if they're ALL zeros
i = LEFT$(nstr, 1) = "0" 'check if the first one is a zero...
DO WHILE i
nstr = MID$(nstr, 2) '...strip it off...
e = e - 1 '...and adjust the exponent
i = LEFT$(nstr, 1) = "0" 'check next digit...
LOOP
END IF
nstr = LEFT$(nstr, d + 1) 'strip off all but the desired SD's + 1
'round off the number...
IF RIGHT$(nstr, 1) >= "5" THEN 'round up...
FOR i = d TO 1 STEP -1
MID$(nstr, i, 1) = CHR$(ASC(MID$(nstr, i, 1)) + 1) ' "elevate" the chr
IF MID$(nstr, i, 1) = ":" THEN 'if a 9 was "elevated" to a ":"...
MID$(nstr, i, 1) = "0" '...make it a 0, then go carry a 1...
ELSE
EXIT FOR '...otherwise, we're done.
END IF
NEXT i
'fix-up an elevated MSD ...
IF LEFT$(nstr, 1) = "0" THEN 'The MSD changed from a 9 to a 0...
nstr = "1" + nstr '...carry the 1...
nstr = LEFT$(nstr, LEN(nstr) - 1) 'loose the LSD to keep things even
e = e + 1 'adding a new MSD moves the radix
END IF
END IF
'we're done with the extra digit -- so now we can toss it ...
nstr = LEFT$(nstr, LEN(nstr) - 1)
'for testing ... prints the number in scientific notation...
'PRINT LEFT$(nstr, 1) + "." + MID$(nstr, 2) + " x 10 ^" + STR$(e)
'add prefix and normalize value...
FOR i = 12 TO -12 STEP -3
IF e >= i THEN
e = e - i
outu = MID$(prefix, i 3 + 5, 1)
EXIT FOR
END IF
NEXT i
IF e <= -13 THEN 'the value is to small to be useful in practice
pformat = " < 1 p" + u
EXIT FUNCTION
END IF
outu = " " + outu + u
'format the mantissa and pad with blanks and zeros where needed...
SELECT CASE e
CASE IS = d - 1 'radix is at immediate right of SD's - just show it
outv = " " + sign + nstr
CASE IS < d - 1 'radix is in the middle of the SD's
outv = sign + LEFT$(nstr, e + 1) + "." + MID$(nstr, e + 2)
CASE IS < 0 'radix is to the left of the SD's - pad with left zeros
outv = sign + "0." + STRING$(ABS(e) - 1, "0") + nstr
CASE IS >= d 'radix is to right of SD's - pad with right zeros
outv = " " + sign + nstr + STRING$(e - d + 1, "0")
END SELECT
pformat = outv + outu
END FUNCTION
[/code]
AND THEN I was thumbing through the VBDOS manuals and came across the FORMAT$ function (not available in QB4.5 and slightly different in your 7.1) that does most of the work for me! I was pissed that I spent so much time on the thing when it could have been so much easier! Oh well. I will save the first version for use with QB4.5. The new version:
[code]
DECLARE FUNCTION pformat (n AS DOUBLE, d AS INTEGER, u AS STRING) AS STRING
DO
INPUT a#
PRINT pformat(a#, 3, "lightyears")
LOOP
FUNCTION pformat (n AS DOUBLE, d AS INTEGER, u AS STRING) AS STRING
'this function requires VBDOS (or possibly QB7.1 with some
'modification -- change FORMAT$ to FORMATD$ and ???
'the unit prefixes: pico, nano, micro, milli, (none), Kilo, Mega, Giga, Tera
CONST prefix = "pnum KMGT"
DIM fmt AS STRING 'the format specifier string
DIM nstr AS STRING 'string representaion of the number
DIM sign AS STRING 'the sign of the number
DIM outv AS STRING 'output value
DIM outu AS STRING 'output units
DIM e AS INTEGER 'scientific notation exponent of 10
DIM i AS INTEGER 'general purpose counter
DIM t AS INTEGER 'temporary general purpose
fmt = STRING$(d, "#") + "E-##" 'create a VBDOS/QB7.1 format specifier
nstr = FORMAT$(n, fmt) 'get the scientific version of n
e = VAL(MID$(nstr, d + 2)) 'separate the exponent
nstr = LEFT$(nstr, d) 'separate the (already rounded!) mantissa
e = e + d - 1 'normalize the mantissa -- dp after MSD
'for testing ... prints the number in scientific notation...
'PRINT LEFT$(nstr, 1) + "." + MID$(nstr, 2) + " x 10 ^" + STR$(e)
'add prefix and normalize value...
FOR i = 12 TO -12 STEP -3
IF e >= i THEN
e = e - i
outu = MID$(prefix, i 3 + 5, 1)
EXIT FOR
END IF
NEXT i
IF e <= -13 THEN 'the value is to small to be useful in practice
pformat = " < 1 p" + u
EXIT FUNCTION
END IF
outu = " " + outu + u
'format the mantissa and pad with blanks and zeros where needed...
SELECT CASE e
CASE IS = d - 1 'radix is at immediate right of SD's - just show it
outv = " " + sign + nstr
CASE IS < d - 1 'radix is in the middle of the SD's
outv = sign + LEFT$(nstr, e + 1) + "." + MID$(nstr, e + 2)
CASE IS < 0 'radix is to the left of the SD's - pad with left zeros
outv = sign + "0." + STRING$(ABS(e) - 1, "0") + nstr
CASE IS >= d 'radix is to right of SD's - pad with right zeros
outv = " " + sign + nstr + STRING$(e - d + 1, "0")
END SELECT
pformat = outv + outu
END FUNCTION
[/code]
Both seem to work ok but still need some more testing. I put the second version in a Lowpass Filter calculator here:
ftp://ftp.sonic.net/pub/users/amckenna/QBASIC/lowpass/
if your are interested. It is a VBDOS program but of course you can read the QB source that is in the LOWPASS.FRM file.
Although I don't mean to imply that Back to the Future is in the same class as Casablanca or The African Queen, it is one of those movies that you can watch over and over again. I hate to put part 1 in the DVD because then I will want to spend the time to watch the whole trilogy!
Thanks for the code / ideas.
Enjoy!
rg