The problem was to emulate monadic format but avoid any use of exponential notation. Here's my solution:
Initially, I thought I could arithmetically round the numbers to #PP significant digits, format them with something ample like 250 125{format}N, and then delete trailing zeros and excess blanks. However, I found that roundoff error causes many numbers to be formatted with lots of 999s or noise in low order digits, which confounds the zero trimming. To get around this problem, I ended up formatting the argument as a vector using dyadic format with a custom (fieldwidth,digits) pair for each element of the argument. The digits value is chosen to show just the desired number of decimal digits (causing APL to do the rounding for us). The formatted vector is reshaped into a matrix, and then trailing zeros and excess blanks are deleted. Here's an example:
+ A{<-}3 2{rho}0.001234 12 1239.56 {neg}1234 0 300 0.001234 12 1239.56 -1234 @ the matrix to format 0 300
(I'm using monadic + here to display values, and I've manually translated {neg} signs to - in the output.) The first step is to count digits (being careful to avoid {log}0):
+ G{<-}1+{floor}10{log}|A+A=0 -2 2 4 4 1 3
If an element of G is >0, it's the number of digits to the left of the decimal in A; if it's {<=}0, -G is the number of consecutive zeros to the right of the decimal. Let P be the number of significant digits we wish to display. The number of digits we should show to the right of the decimal is:
P{<-}3 + D{<-}0{max}P-G 5 1 0 0 2 0
To compute the minimum required field width, we add up the widths of the various parts of the number:
W{<-}1+D+1{max}G @ one blank to left plus all digits W{<-}W+(A<0)+D{/=}0 @ negative sign and decimal point W 8 5 5 6 5 4 (,W,[2.5]D){format},A 0.00123 12.0 1240 -1234 0.00 300
(As I mentioned in the original problem description, I'm not going to round to the left of the decimal, so -1234 will be displayed with four significant digits even though P is 3.) To get the numbers to line up properly in the matrix, we need to make the width uniform in each column, as in:
+ U{<-}({rho}W){rho}{max}{slashbar}W 8 6 8 6 8 6 3 14{rho}(,U,[2.5]D){format},A 0.00123 12.0 1240 -1234 0.00 300
But before doing this, we have to increase the field widths to compensate for the shift that will be needed to align the decimal points. For example, the first column will need to be shifted like so:
0.00123 1240 0.00
Thus, the width for 1240 must be increased by 6 and the width for 0.00 increased by 3. In general, the increase is:
T{<-}(({rho}D){rho}{max}{slashbar}D)-D T{<-}T+D=0 @ one more if decimal is absent T 0 0 6 2 3 2 + W{<-}W+T @ required field widths 8 5 11 8 8 6 + W{<-}({rho}W){rho}{max}{slashbar}W @ make each column uniform 11 8 11 8 11 8
The shape of the formatted matrix is given by:
+ G{<-}(1{take}{rho}A),+/W[1;] 3 19 G{rho}(,W,[2.5]D){format},A 0.00123 12.0 1240 -1234 0.00 300
We can shift the decimals into alignment by adjusting the field widths. For example, to move 1240 six spaces to the left, we reduce its field width by 6. The next number (-1234) needs to be moved two spaces to the left, but the shift of 1240 will have already moved it six to the left, so we need to increase its field width by 4. (In other words, reduce its field width by 2-6, the decimal shift for the field minus the shift for the previous field.) The required adjustment for all fields is:
+ S{<-}T-({rho}T){rho}{neg}1{drop}0,,T 0 0 6 -4 1 -1 + W{<-}W-S 11 8 5 12 10 9 Z{<-}G{rho}(,W,[2.5]D){format},A Z 0.00123 12.0 1240 -1234 0.00 300
The adjustment for the last number may leave the formatted vector too short, so in general we'll need to use G{rho}({times}/G){take} to create the matrix.
The next step is to remove trailing zeros to the right of the decimal. I used "partition" operations to do this. A partition operation is an APL operation, such as {or}/ or ^\, that's applied to each segment of a multi-segment vector. The start of each segment in the data vector is indicated by a 1 in a corresponding "partition vector". For example:
1 1 0 1 0 0 1 0 1 data vector 1 0 0 0 1 0 1 1 0 partition vector 1 1 0 0 0 0 1 0 0 partitioned ^\ 1 0 1 1 partitioned {or}/
Partition operations are carried out by subroutines (such as pANDSCAN and pORRED) that can be found in Bob Smith's paper "A Programming Technique for Non-Rectangular Data" in the APL79 Conference Proceedings (APL Quote Quad, Vol. 9, No. 4).
To avoid having to do reverse scans, I reverse the formatted matrix before raveling it. The start of each partition is found by locating non-blanks preceded by a blank:
V{<-},{reverse}Z B{<-}V{/=}' ' @ 1s mark nonblanks F{<-}B>{neg}1{drop}0,B @ 1s mark the first char of each number V,[.5]1 0{format}F 0.21 32100.0 4321- 0421 003 00.0 100000001000000000000100000000000100000010000000010000000
(Note that >, not usually thought of as a Boolean operation, is used here to locate places where the left argument is 1 and the right argument is 0.) After comparing V with '0', a partitioned ^\ identifies the leading zeros in V (which correspond to trailing zeros in the formatted matrix).
T{<-}F pANDSCAN V='0' V,[.5]1 0{format}T 0.21 32100.0 4321- 0421 003 00.0 100000000000000000000000000000000100000011000000011000000
However, not all leading zeros should be deleted--only those in numbers containing a decimal point. To avoid deleting necessary zeros, we turn off the partition bit for any number that does not contain a decimal:
F{<-}F\F pORRED V='.' T{<-}F pANDSCAN V='0' V{commabar}1 0{format}F,[.5]T 0.21 32100.0 4321- 0421 003 00.0 100000001000000000000000000000000000000000000000010000000 100000000000000000000000000000000000000000000000011000000
If we delete all the zeros to the right of a decimal, we should also delete the decimal point:
D{<-}T<{neg}1{drop}0,T @ 1s mark char just past each group of 0s T{<-}T{or}D\'.'=D/V @ delete adjacent decimal V,[.5]1 0{format}T 0.21 32100.0 4321- 0421 003 00.0 110000000000000000000000000000000000000000000000011100000
We eventually want to get rid of excess blank columns (which may be introduced by blanking out zeros), so we'll also mark blanks in V:
T{<-}T{or}V=' ' @ delete blanks, too T{<-}~T @ now 0s mark stuff to delete D{<-}({rho}Z){rho}T @ make it a matrix like Z 1 0{format}D 0011000011111110000 0011111000000011110 0011100000000010000 {reverse}Z 0.21 32100.0 4321- 0421 003 00.0
To delete excess blanks, we remove all blanks by compressing with T, then insert just the right number of blanks by expanding with a suitable vector E. A reshape and a reversal gives us the finished result:
E{<-}(B{or}{neg}1{drop}0,B{<-}{or}{slashbar}D)/D V{<-}({rho}E){rho}(,E)\T/V {reverse}V 0.00123 12 1240 -1234 0 300
The complete program includes a bit more stuff to handle edge conditions like empties, non-matrix arrays, arguments or columns without any decimals, blanks at the end, etc. Here it is, along with the required partition subroutines from Bob Smith's paper:
{del} Z{<-}P NOXFMT A;B;D;E;F;G;S;T;V;W;#CT;#IO [1] @Monadic {format}, without exponential notation [2] @ Uses "modern" style formatting, with only one blank between adjacent [3] @ numeric columns and no leading or trailing blank columns. [4] @ [5] #IO{<-}1 & #CT{<-}0 [6] {execute}(0#NC'P')/'P{<-}#PP' @ optional left arg is #PP surrogate [7] S{<-}{rho}A [8] {->}(~0{epsilon}S)/L1 @ If arg is empty, [9] Z{<-}S{rho}'' @ return same-shape empty as per {+ +}standard [10] {->}0 [11] L1:A{<-}(({times}/{neg}1{drop}S),{neg}1{take}1,S){rho}A @ work with a {+ +}matrix [12] G{<-}1+{floor}10{log}|A+A=0 [13] @ |G is the number of digits to the left of the decimal (if G>0) or [14] @ the number of consecutive zeros to the right of the decimal (G{+ +}{<=}0) [15] [16] @ Compute the appropriate Width and Digits format for each number: [17] D{<-}0{max}P-G @ num digits we'll put to right of {+ +}decimal [18] @ Compute required field widths: [19] W{<-}1+D+1{max}G @ one blank to left plus all digits [20] W{<-}W+(A<0)+D{/=}0 @ negative sign and decimal point [21] T{<-}(({rho}D){rho}{max}{slashbar}D)-D @ shift needed to {+ +}align decimals [22] T{<-}T+(D=0)^({rho}D){rho}{or}{slashbar}D>0 @ one more if decimal {+ +}absent in col w. some [23] W{<-}W+T @ increase width to compensate for shift [24] W{<-}({rho}W){rho}{max}{slashbar}W @ make each col have {+ +}uniform width [25] G{<-}(1{take}{rho}A),+/W[1;] @ formatted matrix shape [26] @ Now, cleverly adjust the field widths so as to slide the [27] @ decimal points into alignment: [28] W{<-}W-T-({rho}T){rho}{neg}1{drop}0,,T @ adjust field widths [29] [30] @ Format the numbers, producing almost exactly the desired result: [31] Z{<-}(,W,[2.5]D){format},A @ format each number [32] Z{<-}G{rho}({times}/G){take}Z @ make it a matrix [33] [34] @ Remove trailing zeros to the right of the decimal, [35] @ and delete excess blank columns: [36] V{<-},{reverse}Z @ work with reversed vector [37] V{<-}'. ',V @ kludge to avoid having zero partitions [38] F{<-}B>{neg}1{drop}0,B{<-}V{/=}' ' @ 1s mark first char of {+ +}each number [39] F{<-}F\F pORRED V='.' @ ignore those without a decimal [40] T{<-}F pANDSCAN V='0' @ 1s mark leading (nee trailing) 0s [41] T{<-}2{drop}T & V{<-}2{drop}V @ undo the kludge [42] D{<-}T<{neg}1{drop}0,T @ 1s mark char just past each {+ +}grp of 0s [43] T{<-}T{or}D\'.'=D/V @ delete adjacent decimal [44] T{<-}T{or}V=' ' @ delete blanks, too [45] D{<-}G{rho}T{<-}~T @ 0s mark stuff to delete [46] E{<-}(B{or}{neg}1{drop}0,B{<-}{or}{slashbar}D)/D @ when {+ +}expanding, put 1 blank between cols [47] V{<-}({rho}E){rho}(,E)\T/V @ delete 0s and blanks, insert {+ +}minimal blanks [48] V{<-}{reverse}0 {neg}1{drop}V @ strip final blank, {+ +}undo reversal [49] [50] Z{<-}(({neg}1{drop}S),{neg}1{take}{rho}V){rho}V @ restore {+ +}leading dimensions {del} {del} Z{<-}P pORRED V [1] @{or}/ of each segment of {omega} defined by first marks {alpha} [2] Z{<-}(V{or}P)/P [3] Z{<-}(Z/1{rotate}Z){<=}P/V {del} {del} Z{<-}P pANDSCAN V;T [1] @^\ of each segment of {omega} defined by first marks {alpha} [2] Z{<-}~(T{<-}V{<=}P)/V [3] Z{<-}~{/=}\T\Z{/=}{neg}1{drop}0,Z {del}Jim
Home Page