Exploring Quattro Pro Formulas, Functions, and Macros
Exploring Quattro Pro Formulas, Functions, and Macros
Formulas, Functions,
and Macros
compiled by
Charles M. Cork, III
Revised October 17, 2015
Contents
Introduction
The reason for this book . . . . . . . . . . . . . . . . . . . . . . . .
Conventions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
QP Quirks, Generally . . . . . . . . . . . . . . . . . . . . . . . . . .
i
ii
iii
iv
1 Basic Formulas
Relative v. absolute addresses . . . . . . . . . . . . . . . . . . . . .
Relative (R1C1) v. normal reference style . . . . . . . . . . . . .
Text (string) formulas . . . . . . . . . . . . . . . . . . . . . . . . . .
How to set up a database showing interest compounding monthly
How to type the date of every Friday in a year in a column . . . .
3
4
6
7
8
9
11
11
ii
14
14
14
15
16
17
17
18
19
20
II
21
3 Conditional functions
25
@IF . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
@CHOOSE . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
4 The Properties of a Cell, Page, etc.
@CELL, @CELLPOINTER, @CELLINDEX . . . . . . . . .
@IS*** Logical Functions . . . . . . . . . . . . . . . . . . .
@TYPE . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
@PROPERTY . . . . . . . . . . . . . . . . . . . . . . . . . .
@COMMAND . . . . . . . . . . . . . . . . . . . . . . . . . .
How (not) to test whether a cell is blank . . . . . . . . . . .
How to enter multiple lines of text in a single cell . . . . .
How to determine the width and height of a row in inches
Appendix: Cell (Active_Block) Properties . . . . . . . . . .
Appendix: Worksheet (Active_Page) Properties . . . . . . .
Appendix: Notebook Properties . . . . . . . . . . . . . . . .
Appendix: Application Properties . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
27
27
29
30
30
31
32
33
33
34
35
36
37
.
.
.
.
.
.
.
.
.
.
.
.
.
.
41
41
44
45
45
46
47
47
.
.
.
.
.
.
.
.
.
.
.
.
49
49
49
49
50
50
51
51
52
52
53
7 Math Functions
57
Adding: @SUM, @TOTAL, @SUBTOTAL, @SUMIF . . . . . . . . 57
Averaging: @AVG, @PUREAVG . . . . . . . . . . . . . . . . . . . . 58
58
59
59
60
61
61
62
65
65
66
66
67
67
68
69
69
69
71
72
74
77
77
78
79
79
80
81
81
82
82
83
84
84
85
85
III
119
121
121
123
123
124
126
126
127
129
131
132
132
132
133
133
134
134
136
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
145
145
145
146
147
149
149
150
150
151
152
153
154
156
156
157
.
.
.
.
160
161
162
163
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
helper
. . . . .
187
188
189
198
200
202
203
204
221
221
222
224
225
226
227
228
231
233
234
236
239
240
243
244
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
247
Introduction
This online book aims to instruct on the use of Quattro Pro (QP) formulas, @functions, and macros, using examples compiled from personal
work and online discussions in several venues. Some of these come from
discussions on Corel newsgroups or its Office Community forums (OC), but
most come from WordPerfect Universe forums (WPU). Much of the content
of this compilation comes from the work of others, and I would call out for
particular commendation Kenneth Hobson, Roy Lemoto Lewis, Uli (full
name unknown, but a regular contributor to the Corel QP Newsgroups), Dr.
David Seitman, and Jeff Barnes. I also want to thank Dr. Seitman for many
corrections of an earlier version of this text. The errors that remain in this
text are exclusively my own.
This book is for readers who have a basic knowledge of QP procedures
and concepts.
The simulations of QP spreadsheets in this book are provided by tables
created in WordPerfect (WP).
This text is an evolving work in progress. Additions and corrections will
be made in later versions. Suggestions for improvements should be e-mailed
to me. The most current versions of these documents will be found here.
This book is placed in the public domain. It is offered AS IS, without
warranties of any sort. The recommendations in this book work for me, but I
make no guarantees about whether how they will work for you. Use them at
your own risk, and modify them at your own risk. I offer them at no charge,
and completely for whatever value you may find in them. If you find this
distribution to be of benefit, I simply ask that you remember the poor (Gal.
2:10) and consider giving to appropriate charitable organizations for their
relief.
Typesetting is courtesy of LATEX.
ii
Conventions
The following conventions are used throughout this book:
Formulas, functions, and macro commands will be distinguished in the
main text by this font.
@Function names are in uppercase letters, but you can use uppercase
or lowercase letters. QP renders them in uppercase.
Macro commands are enclosed in braces, e.g., {}. You may use any
combination of uppercase or lowercase letters. I combine upper and
lower case for readability.
Arguments for @functions and macros are in italics, mostly lowercase.
Example: @TIME(hour,minutes,seconds)
Optional arguments for @functions and macros are in angle brackets
<>. Example: @LASTCELLVALUE(block,<type>)
Keyboard keys and on-screen buttons are enclosed in simple brackets.
Examples: [Enter] and [OK]. Control keys to be pushed simultaneously are shown with a plus sign, as in [Shift+F12].
Menu selections are noted by separating higher-level from lower-level
menu items with the right angle bracket. Example: Edit >Clear >Format.
I will attempt terminological consistency with related terms:
Over time, what we refer to as text was first referred to as a label and
then as string. I will use text to refer to it generically, and Ill use string
to refer to specific, ascertainable sets of text characters. My name
Charlie is text, and the seven letters in it form a particular string of
text.
I will use block as the most general term to refer to a set of one or
more contiguous cells definable by coordinates such as A1..Z10. I will
use table to refer to a block that has some unity of purpose, and a
database as a table with the specific purpose of structuring the storage
and retrieval of data.
I will use tables as illustrations of QP screens. In these, I will illustrate
the technique of entering a formula in one cell, and then copying it to a block
of cells, by highlighting the first cell in cyan and the target block in yellow.
Where the target block includes the first cell, it will remain in cyan. It will
look like Table 1.
iii
1
2
3
4
5
6
7
8
Formula in A1 is copied to A2..A8.
Formula in C1 is copied to D1..H1.
Formulas in B3..B5 are copied to the right, either separately, or collectively by selecting B3..B5, copying (Ctrl+C), and pasting to C3..D3, which fills C3..D5.
Formula in E6 is copied to E6..H8.
Displays of grids will usually show the return values of formulas, rather
than the formulas themselves. The formulas/functions themselves will typically appear in the main text or in the block below the grid, but sometimes,
the formula/function in one cell will be typed into another cell with arrows
( or ) indicating the relationship.
I will also use highlighting on complex formulas, either to show how they
are progressively constructed or to show different components.
Because some formulas and macros are too long to fit in a cell on the
screen, I have broken them onto separate lines. As long as they are in the
same cell or indented block, the reader may assume that they are intended
to run together in a single cell.
QP Quirks, Generally
Most QP functions and macros work well and precisely as described in
its help materials, but unfortunately, some do not. Particular @functions
and macros will be discussed in the chapters on each. Here, I note some
problems that exist across the system. These are largely a problem of factors
mentioned at the start of this introduction.
Named Blocks
Many spreadsheet users name a cell or block of cells using [Ctrl+F3]
(right-clicking on the cell displays a pop-up menu that also allows you to
name the cell), and many guides use such named cells in lieu of referencing
them by coordinates. I have been persuaded to avoid using named blocks as
iv
much as possible, and therefore the functions and macros in this book will
refer to blocks of cells by their addresses, not their block names.
In particular, the names can oddly drift" so that the name for block A will
suddenly appear in functions to refer to block B, but the functions generally
work correctly. And more seriously, deleting a named block sometimes causes
document corruption. The user who is interested in pursuing the quirks of
block names in QP should consult these threads: WPU 33831, WPU 26708,
WPU 24258, WPU 19272, WPU 16099.
I do not entirely avoid named ranges. They can be useful for expediting
the launching of macros, as noted below at page 132.
Many of them can be replaced with @@ functions. See page 46, below.
vi
Part I
Chapter 1
Basic Formulas
At the most basic level, when you place a mathematical formula in a cell,
QP will automatically calculate the numeric result, as shown by formulas in
column A in Table 1.1.
Table 1.1: Basic formulas
A
1
12
2
-5
3
21
4 3.1428571
5
27
6
9
7
2
B
What 7+5 in A1 returns
What 7-12 in A2 returns
What 3*7 in A3 returns
What 22/7 in A4 returns
What 33 (3 cubed) in A5 returns
What 81(1/2) (square root of 81) in A6 returns
What (3*4)/6 in A7 returns
B
What +A1 in A2 returns
What +A1+9 in A3 returns
What +A1/4 in A4 returns
What (A1/6)*10 in A5 returns
What +A1*A2 in A6 returns
What +A1= 12 in A7 returns (namely, true)
What +A1>10 in A8 returns (true)
What +A1<10 in A9 returns (false)
At the next basic level, illustrated in Table 1.2, QP formulas return the
results of such operations based on the content of cells referred to in the
3
formula. The content of the referenced cell is a variable; changing its content
changes the results of the formula. Note the + sign in these examples. If you
simply enter A1 into A2, QP stores A1 as text. Some sign is necessary to
tell QP that you want A1 to refer to the content of a cell rather than to start
a text string. Any of these would refer to A1 without altering its meaning:
+A1, =A1, or (A1). More than one cell can be referenced in a formula, as
shown by the formula in A6.
When the user simply changes the number in A1, QP recalculates all
formulas, as shown in Table 1.3.
Table 1.3: Recalculation of formulas
A
1
30
2
30
3
39
4 7.5
5
50
6 900
B
What +A1 in A2 now returns
What +A1+9 in A3 returns
What +A1/4 in A4 returns
What (A1/6)*10 in A5 returns
What +A1*A2 in A6 returns
1
2
3
4
5
6
A
1
2
3
4
5
6
B
+A1+1
A2 pastes in A3 as +A2+1
A2 pastes in A4 as +A3+1
A2 pastes in A5 as +A4+1
A2 pastes in A6 as +A5+1
C
7
14
21
28
35
42
D
+A1*7
D1 pastes in D2 as +A2*7
D1 pastes in D3 as +A3*7
D1 pastes in D4 as +A4*7
D1 pastes in D5 as +A5*7
D1 pastes in D6 as +A6*7
example:
If a cell containing a formula that references A1 is copied, say, four
rows down from the original cell, the reference to A1 will be adjusted
to A5.
If the cell is copied, say, four columns to the right from the original cell,
the reference to A1 will be adjusted to E1.
If the cell is copied both four rows down and four columns to the right
from the original cell, the reference to A1 will be adjusted to E5.
The same relative changes occur when the cell is copied to the same
sheet in another notebook.
The same relative changes occur for each cell if more than one cell is
referenced in the initial formula.
Sometimes, however, you do not want the copying and pasting to do this,
as where you want to apply the percentage rate in a single cell to a range
of numbers. In that case, you want to make an absolute reference to
that cell. Absolute references are marked by adding the dollar sign to each
component of the cell. In Table 1.5, we want to apply the rate in A2 to the
numbers in B1..D1. The incorrect relative reference, +A2*B1, is created in
B2 and then copied to C2..D2. The formula in C2 multiplies C1 not by A2,
but by B2. The formula in D2 multiplied D1 not by A2, but by C2. The
correct absolute reference, +$A$2*B1, is created in B3, and then copied to
C3..D3. Formulas are adjusted as desired: B1, C1, and D1 are multiplied
solely by A2.
Table 1.5: Incorrect and correct formulas for pasting
A
1 Rate
2 0.25
3
B
100
25
25
C
500
12500
125
D
1000
12500000
250
0.25
0.35
0.45
B
100
25
35
45
C
500
125
175
225
D
1000
250
350
450
the first row. But the row of the rate and the column of the other number
will automatically adjust relative to the initial cell.
A shortcut method of making absolute or partially absolute references
involves using the [F4] key. While typing a formula referencing cell A2, for
instance, while the insertion point is in or just after A2, pressing [F4] will
convert A2 into $A:$A$2. Pressing [F4] multiple times will cycle through
seven different absolute or partially absolute variations, that are detailed in
the QP help file under Working with formulas and functions.
When these references are actually placed in a cell, QP converts (translates) them into coordinates on the grid in the normal reference style. They
6
Since they are more complex than normal references and since QP translates them anyway, this type of reference is not useful for manually entering
formulas. However, it is quite convenient when constructing formulas to be
put into cells by automated means, particularly with macros. (Note, however,
that in macros, additional brackets may be required. See page 122.)
B
2015
January 2015
C
2015
ERR
D
2015
January 2015
+A1 in A2.
+A2&""& B1 in B2.
+A2&""& C1 in C2.
@CONCATENATE(A2,"",D1) in D2.
0.18
Beg Balance
$100.00
$101.50
$103.02
$104.57
$106.14
$107.73
Int
$1.50
$1.52
$1.55
$1.57
$1.59
$1.62
End Balance
$101.50
$103.02
$104.57
$106.14
$107.73
$109.34
The informational data are set up in A1..A50 and B2..D2, as shown. Put the
interest rate (0.18) in cell B1. Put the starting principle (100) in B3. Format
B3..D50 as currency. Now for the formulas:
1. Place +B3*($A:$B$1/12) in C3. This calculates the monthly interest
on the principle by multiplying the current balance by the yearly
interest rate, divided by 12 to get the monthly interest rate. The
absolute reference to B1 allows the rate in B1 to copy to all the lower
cells in the C column.
2. Place +B3+C3 in D3 to get the balance at the end of the month.
8
10
Chapter 2
B
1
2
3
4
5
6
7
8
9
10
C
6
7
8
9
10
11
12
13
14
15
B1 contains +A1..A10+5.
E1 contains +A1..A10/2.
D
-1
0
1
2
3
4
5
6
7
8
E
2
4
6
8
10
12
14
16
18
20
C1 contains +A1..A10-2.
F1 contains +A1..A10=5.
11
F
0.5
1
1.5
2
2.5
3
3.5
4
4.5
5
G
0
0
0
0
1
0
0
0
0
0
0
0
0
0
0
1
1
1
1
1
D1 contains +A1..A10*2.
G1 contains +A1..A10>5.
B
2014
2014
2014
2014
2015
2015
2015
2015
Q1
Q2
Q3
Q4
Q1
Q2
Q3
Q4
D
113
134
160
156
163
157
170
173
D1 contains +B1..B8=Q1.
E
1
0
0
0
1
0
0
0
113
0
0
0
163
0
0
0
E1 contains (C1..C8)*(B1..B8=Q1).
(C1..C8)*(B1..B8="Q1")*(A1..A8=2015)
Concluding comments
In my judgment, block formulas are a more useful way to extract information from a database than @SUMIF and @COUNTIF functions that yield
13
1
2
3
4
5
6
7
8
A
Home
Home
Away
Home
Away
Away
Away
Home
B
Even
Odd
Even
Odd
Even
Odd
Even
Odd
C
113
134
160
156
163
157
170
173
1
2
3
4
5
6
7
8
A
Home
Home
Away
Home
Away
Away
Away
Home
B
Even
Odd
Even
Odd
Even
Odd
Even
Odd
C
113
134
160
156
163
157
170
173
D
113
247
407
563
726
883
1053
1226
E
113
247
247
403
403
403
403
576
D1 = @SUM($C$1..C1)
E1 = @SUM(($A$1..A1="Home")*($C$1..C1))
One quick way is to enter +C1 in D1 and +D1+C2 in D2, followed by copying
D2 into D3..D8. Better, though is to place this formula in D1 and copy to
D2..D8:
@SUM($C$1..C1)
This formula always sums the range running from C1, which is set by
absolute reference, to the cell in column C next to the copied formula in
column D.
The second task is to build a formula that keeps a running total of
numbers in the C column that meet a given condition, which here will be
the condition that Home is in the A column. The way to do this is to place
the following formula in E1 and copy it into E2..E8.
@SUM(($A$1..A1="Home")*($C$1..C1))
15
B
50
25
35
40
31
15
20
16
42
19
17
21
D
01/02/12
01/08/12
203
@MIN((A1..A8="Away")*(C1..C8))
returns 0 instead of the desired 157 from C6. It does so because all of
the numbers in C1..C8 that failed the test in A1..A8 were evaluated as
0, and 0 is less than 157. To get the minimum number that meets the
test, we need to disqualify the ones that do not meet the test by artificially
increasing them. Among many possible ways to do so, creating an @IF
function that changes their evaluation upward, and prevents them from
being the minimum number, will work, as in this case:
@MIN(@IF(A1..A8="Away",0,100000)+(C1..C8))
which correctly returns 157. If the number in C1..C8 meets the test, it adds
nothing (0) to that number, but if it does not meet the test, it adds 100,000,
and such numbers will not be the minimum number.
1
2
3
4
5
6
7
8
9
A
11/23/13
11/23/13
11/23/13
11/23/13
11/23/13
11/28/13
11/28/13
11/28/13
11/28/13
C
6
4
8
2
5
7
6
9
3
D
1
1
1
1
1
0
0
0
0
6
4
8
2
5
0
0
0
0
Column C: A1..A9=A1.
Column D column multiplies B1..B9 by C1..C9.
calculates the information that Column D displays and performs the @MAX
value on it.
1
2
3
4
5
6
7
8
9
A
11/23/13
11/23/13
11/23/13
11/23/13
11/23/13
11/28/13
11/28/13
11/28/13
11/28/13
C
6
4
8
2
5
7
6
9
3
D
1
1
1
1
1
0
0
0
0
4
6
2
8
5
0
0
0
0
Column C: A1..A9=A1;
Column D multiplies C1..C9 by 10-(B1..B9).
B
Date
01/08/14
01/17/14
01/17/14
02/07/14
03/04/14
03/04/14
03/10/14
03/11/14
04/07/14
04/08/14
04/08/14
04/10/14
C
Amount
$245.00
$26.40
$211.25
$245.00
$50.00
$50.00
$245.00
$245.00
$245.00
$485.00
$485.00
$245.00
???
???
???
???
C3=@IF(@SUM((B$2..B2=B3)(A$2..A2+7>=A3)),"???","")
We look for numbers in the B column that are identical to prior numbers in
the B column, but the dates in the A column may be off by 7 days. Thus, we
ignore duplicates that are monthly charges. The formula should place ??? in
the C column if a potential duplicate appears. The first possible duplicate
will be on the third row. This formula works:
@IF(@SUM((B$2..B2=B3)*(A$2..A2+7>=A3)),"???","")
The (B$2..B2=B3) component finds each prior match in the B column, and
the inequality (A$2..A2+7>=A3) takes corresponding dates, adds 7 days,
and then if the resulting date is greater than or equal to the date on this
row, a possible duplicate has been identified. The $ anchors allow us to copy
the formula down, parallel with the data.
20
Part II
21
23
24
Chapter 3
Conditional functions
@IF
@IF(Test,Result1,Result0) contains three arguments. The Test
argument tests a proposition that either evaluates as true (1, or apparently
any non-zero number) or false (0). If Test evaluates as true, the function
returns the value specified in Result1; otherwise, it returns the value in
Result0.
The Test argument can be very complicated, but it must evaluate as 1
or 0. It usually involves equations and inequalities (such as =, >, <, >=,
<=, <>), but it can include functions such as the @IS*** functions. Both
sides of an equation in Test may include formulas or functions. Different
equations can be combined in the Test argument by the connectors #AND#
and #OR#. Unsurprisingly, these mean that both equations joined by #AND#
must evaluate as 1 for Test to evaluate as 1; but if either of the equations
joined by #OR# evaluates as 1, Test evaluates as 1.
Result1 and Result0 may include any values. They may include other
formulas and functions. In particular, they may include other @IF functions,
which are then referred to as nested @IF functions. Thus, an @IF test for
whether cell A1 contains a number greater than 0, or less than 0, or equal
to 0, might look like this:
@IF(A1>0,"A1 is greater than 0",@IF(A1<0,"A is less
than 0","A1 equals 0"))
and therefore, if the programmer uses @IF with arrays, the results should
be diligently checked. Instead of using @IF with array values, he suggests
using appropriate block formulas, covered above beginning at page 11.
@CHOOSE
@CHOOSE(Number,ListSeparatedByCommas) returns the item in
ListSeparatedByCommas indicated by Number. The items in the list begin
with 0, so if Number is 1, the function returns the second item, and so on.
The items can be any value (numbers, text, or functions) and they can be
cells containing any values.
This function has great utility when the Number is supplied by a formula
or function that makes it apply in different settings, some of which will
appear below.
For the converse function that returns the offset number from the match,
see @MATCH.
Table 3.1 illustrates the use of @CHOOSE to take a date number (42110,
which is Thursday, April 16, 2015) in A1..A2 and give the full name of the
month and weekday.
Table 3.1: Using @CHOOSE to give information about dates
A
1 04/16/15
2 04/16/15
B
3
5
C
April
Thursday
B1 contains the formula @MONTH(A1)-1, which provides the month number. Because it returns months on a scale of 1 to 12, and @CHOOSE uses a
scale starting at 0, we subtract 1 from it.
C1 contains this function:
@CHOOSE(B1,"January","February","March","April",
"May","June","July","August","September","October",
"November","December")
Starting with January as item 0, this function returns April. The formula
in B1 could have been substituted into the first argument of this function.
B2 contains the function @MOD(A2,7), takes the number in A2, divides
by 7, and if there is a remainder, it returns the remainder. Here, it returns
5.
C2 contains this function:
@CHOOSE(B2,"Saturday","Sunday","Monday",
"Tuesday","Wednesday","Thursday","Friday")
Starting with Saturday as item 0, it takes the number in B2 and returns
Thursday.
26
Chapter 4
Returns the absolute address on the sheet, such as $A$1, as text. If the
function refers to a cell on a different sheet, it will add the sheet name,
such as $B:$A$1.
To get only the column and row, use TwoDAddress.
To always get the sheet as well, use ThreeDAddress.
27
The help file suggests that one can get the notebook name too by
using FullAddress, but that argument seems to do nothing different
from ThreeDAddress. It most definitely does not return the file name.
(Hat tip to David Seitman for pointing this out.)
To get the notebook path and name alone, use NotebookPath.
row
col
Returns the column number, starting from 1 (column A), 2 (B), etc.
sheet
Returns the sheet number, starting from 1 (column A), 2 (column B),
etc., even if the sheets are given specific names.
contents Returns the unformatted contents of the cell. For instance, the date
formatted as 01/31/15 would be returned as 42035.
type
prefix
Returns the prefix, if any, for the contents of the cell. In earlier versions,
cells were aligned by preceding them with a single-quote, for left
alignment, a double-quote, for right alignment, or a caret, for centered
alignment. A single character (usually - or =) could be repeated across
the cell by preceding it with a backslash.
width
rwidth
protect
format
B
address
$A$2
$A$3
$A$4
$A$5
$A$6
$A$7
C
col
1
1
1
1
1
1
D
row
2
3
4
5
6
7
E
contents
Text
Text
42035
42035
123.45
0
F
type
l
l
v
v
v
b
G
prefix
H
format
G
G
G
D4
C2
G
A2 and A3 contain the word "Text", with and without a centering prefix.
A4 and A5 contain the same number, with or without date formatting.
A6 contains a number formatted as currency.
A7 is blank. Attributes are in B1..H1.
The function @CELL(B$1,$A2) was entered in B2, then copied to B2..H7.
@ISBLANK(B1)
@ISBLOCK(B1)
@ISERR(B1)
@ISEVEN(B1)
@ISLOGICAL(B1)
@ISNA(B1)
@ISNONTEXT(B1)
@ISNUMBER(B1)
@ISODD(B1)
@ISSTRING(B1)
B
0
0
1
0
1
1
0
1
1
0
0
C
1
0
1
0
0
1
0
1
1
1
0
D
2
0
1
0
1
0
0
1
1
0
0
E
Text
0
1
0
ERR
0
0
0
0
ERR
1
1
1
0
ERR
0
0
1
1
ERR
0
0
1
0
ERR
0
0
0
0
ERR
1
H
ERR
0
1
1
ERR
ERR
0
1
1
ERR
0
I
NA
0
1
0
ERR
NA
1
1
1
ERR
0
J
A1..A20
0
1
0
ERR
0
0
0
0
ERR
1
Quirks.
@ISBLANK, @ISEVEN, @ISLOGICAL, @ISNONTEXT, and @ISODD all wrap
themselves in an @ARRAY function, needlessly, as far as I can tell. The
help files do not reflect that these functions require an @ARRAY wrapper;
29
they affirmatively assert that the functions return correct values without
any further elaboration. This is particularly significant in crafting more
complex functions and in drafting macros, some or all of which fail without
the @ARRAY wrapper.
@ISBLOCK is rendered @Iblock (mixed case) on the screen, and it behaves differently from other functions. In the other cases, when the function
contains a cell as an argument, it tests the content of that cell. @ISBLOCK
tests only whether its argument is a valid cell or block. If so, it returns
1; if not, it returns 0. (Hat tip to David Seitman for correcting my initial
discussion of @ISBLOCK.
See discussion in OC 4993.
@TYPE
A variation on the @IS functions is @TYPE(Value/Cell). I cant think
of a use for it, but when applied to a value or cell, it returns 1 if the value/cell
is numeric, logical, or empty; 2 if it contains text, 16 if it is ERR, and 64 if
it is an array. The help file says that it can be used to test if a function is
receiving the type of value that it expects to receive.
@PROPERTY
@PROPERTY("Object.Property") returns a value about some component of the QP notebook: a cell, a sheet, or the notebook itself. The user
must compose text that identifies first the object, then the property, and
optionally, sub-properties, all separated by periods. I have placed several
appendices showing what properties are available at the end of this section.
The function returns values as text, even if they look like numbers.
The Object can be:
a cell (e.g., A1) or block (e.g., A1..A5), or a function that returns a cell
as text.
Active_Block, which returns values about the currently selected cell
or block.
Active_Page, which returns values about the current sheet.
Active_Notebook, which should be self-explanatory.
Application (undocumented).
30
And though the Object can be a specific cell or block in the active
notebook other than the selected cell or block, page-level and notebooklevel properties can only be read on the active sheet and notebook. Thus,
specifying a cell on another page as the Object will only allow us to get the
properties of that cell, not the page in which it is located.
The QP help file says that the Property is a string and provides a link
to the main help file, which contains a large array properties grouped into
categories and subcategories. An appendix to this section looks at those
properties that apply at four levels: Cell/block; Page (Sheet); Notebook;
Application. I have not experimented with the properties that apply to
charts, dialogs, menus, and other objects.
Quirks.
Although @PROPERTY can use a block of cells as the Object, it does not
return values in an array, so almost all of the information that it returns is
valid only for the first cell in the block.
Attempting to determine whether a row or column is hidden fails. See
the workaround in WPU 31127, which relies on a combination of macro
commands ({OnErr} and {EditGoTo}) to detect hidden cells.
My sense is that it takes QP a long time to evaluate the argument, and
therefore I do not use @PROPERTY in macros that rapidly and repetitively
use this function.
@COMMAND
@COMMAND(CommandEquivalent) performs largely the same functions as @PROPERTY. It gets the same settings for pages/sheets and notebooks that @PROPERTY gets. It also allows the user to get settings of the
QP application itself (but @PROPERTY does the same without the help files
documenting that fact). It does not get settings for individual cells, and
it cannot be used to get settings for pages and notebooks other than the
current one. It returns values as text, even if they look like numbers.
The help file misleadingly suggests that one can get a list of the
command equivalents that are acceptable arguments by pressing
[Shift+F3]. As it turns out, the format is exactly the same as the format for @PROPERTY arguments, except the object is either:
Page (not Active_Page)
Notebook (not Active_Notebook)
Application
31
B
blank in b2
1
1
1
1
1
1
C
zero in c2
0
1
1
0
0
1
0
D
@TRIM(" ") in D2
1
1
1
0
1
0
All six tests applied to the true blank cell in B2 correctly return 1,
indicating that it is blank. Surprisingly, however, a zero in cell C2 tests as
if it is blank for the tests in A3, A4, and A7. If the user wants to exclude
a cell with a zero in it from consideration as blank, those tests must not
be used. Instead, only the tests in A5, A6, and A8 give the desired result.
As for functions that return empty strings, or cells containing only prefixes,
the user may or may not want to treat them as blank. If they may count
as blank, the user may use only the formulas in A3, A4, A5, and A7. If the
user wants to treat those cells as non-blank, only A6 and A8 give the desired
result.
The take-away is this:
To get all and only the cells that have absolutely nothing in them, only
@CELL("type") and @ISBLANK can be used (see rows 6 and 8). And
if the user wants to apply the formula to a block (array) of cells, the
32
user must use only @ISBLANK, because @CELL does not work on blocks,
only on individual cells.
If the user wishes to count as blank cells that include a formula that
evaluates as an empty string but not count the entry of 0 as blank,
@COUNTBLANK must be used (see row 5).
If the block to be tested does not contain zeros or formulas that return
empty strings or zeros, then any of these tests will do.
Note the further discussion at OC 4993. Steve Szalai observes that some
references to blank in these functions should rather be empty.
Note also that if the cell coordinates are to be derived via the @@ function
(see the chapter on cell coordinate functions at 41), the text in the function
needs to evaluate as a single cell block, e.g., B2..B2, rather than a single cell
alone, e.g., B2. Hence, whether B2 is blank is determined by:
@COUNTBLANK(@@(@OFFSET(A1,1,1)))
But not by:
@COUNTBLANK(@@(@ADDRESS(2,2)))
@VALUE(@FIELD(@PROPERTY("A1.row_height"),2,
","))/1440
Hat tip to David Seitman, Roy Lewis, and Martin (?). For a lengthier
discussion, see WPU 30996. For Dr. Seitmans impressive development that
determines the block of cells that fit in the QP window, see WPU 35788.
1
2
A
A1.
General,Bottom,No,
Horizontal,0,0,0
Properties
Alignment
General
Alignment.Horizontal
4
5
6
7
Horizontal
Bottom
No
Set Width,2370,1
Alignment.Orientation
Alignment.Vertical
Alignment.WrapText
Column_Width
Protect,General
Constraints
General
10 Protect
11 Arial,10,Yes,No,No,No
Constraints.Data_Entry
_Input
Constraints.Protection
Font
12
13
14
15
16
17
Font.Typeface
Font.Point_Size
Font.Bold
Font.Italic
Font.Underline
Font.Strikeout
Arial
10
Yes
No
No
No
34
C
Possible Values (all text)
General | Left | Right | Center | Center Across Block, Top | Center | Bottom,
WrapText?(Yes | No), Horizontal | Vertical
General | Left | Right | Center | Center
Across Block
Horizontal | Vertical
Top | Center | Bottom
Yes | No
Operation, WidthInTwips, ColSpacing
(the help file separates operations)
Protect | Unprotect, General | Labels
Only | Dates Only
General | Labels Only | Dates Only
Protect|Unprotect
Typeface, PointSize, Bold, Italic, Underline, Strikeout
Typeface
PointSize
Yes | No
Yes | No
Yes | No
Yes | No
18 NoChange,NoChange,
NoChange,NoChange,
NoChange,NoChange,
0,0,0,0,0,0
Line_Drawing
19
20
21
22
A1.
General,2,United States,0
NA
Set Height,246
Number_Value
Numeric_Format
Reveal/Hide
Row_Height
23
24
25
26
27
Cell:A1..A1
16,3,Blend1,No
16
3
Blend1
Selection
Shading
Shading.Color_1
Shading.Color_2
Shading.Blend
28
29
30
31
A1.
Normal
3
A1.
String_Value
Style
Text_Color
Value
A
Active_Page.
No,0,1E+300,
4,3,5,4
5
5
6
7
No
4
1E+300
8
9
3
0
1
2
10 960
11 Yes,Yes,Yes,
Yes,Yes
B
Properties
Conditional_Color
Conditional_Color.Above
_Normal_Color
Conditional_Color.Below
_Normal_Color
Conditional_Color.Enable
Conditional_Color.ERR_Color
Conditional_Color
.Greatest_Normal_Value
Conditional_Color.Normal_Color
Conditional_Color.Smallest
_Normal_Value
Default_Width
Display
35
C
Possible Values (all text)
Enable, SmallVal, GreatVal, BelColor,
Normal, AboveCol, ERRCol
0-15
0-15
Yes | No
0-15
GreatVal
0-15
SmallVal
WidthInTwips
DisplayZeros?(Yes | No), RowBorders?(Yes | No), ColBorders?(Yes |
No), HorzGridLines?(Yes | No), VertGridLines?(Yes | No)
12 Yes,Yes
Display.Borders
13
14
15
16
Yes
Yes
Yes
Yes,Yes
Display.Borders.Column_Borders
Display.Borders.Row_Borders
Display.Display_Zeros
Display.Grid_Lines
17
18
19
20
Yes
Yes
ActivePage
No,No
Display.Grid_Lines.Horiz
Display.Grid_Lines.Vertical
Name
Protection
21
22
23
24
No
No
255,255,255,Yes
100
Protection.Cells
Protection.Objects
Tab_Color
Zoom_Factor
1
2
A
Active_Notebook.
Yes,Yes,Yes,Show All
Yes
4
5
6
Show All
Yes
Yes
7
8
9
10
11
12
13
14
15
16
17
Off
No
(a long series of numbers)
16777215
12632256
8421504
0
255
65280
16711680
16776960
B
Properties
Display
Display.Show
_HorizontalScroller
Display.Objects
Display.Show_Tabs
Display.Show_VerticalScroller
Group_Mode
Macro_Library
Palette
Palette.Color_1
Palette.Color_2
Palette.Color_3
Palette.Color_4
Palette.Color_5
Palette.Color_6
Palette.Color_7
Palette.Color_8
36
C
Possible Values (all text)
VertScroll?(Yes | No), HorzScroll?
(Yes | No), Tabs?(Yes | No), Objects
(Show All | Show Outline | Hide)
Yes | No
Show All | Show Outline | Hide
Yes | No
Yes | No
On enables Group Mode
Yes | No
Color1, Color2, ..., Color16
18
19
20
21
22
23
24
25
26
27
16711935
65535
8388736
32768
32896
8388608
128
8421376
None
Background,
Natural,1,Yes,No
Palette.Color_9
Palette.Color_10
Palette.Color_11
Palette.Color_12
Palette.Color_13
Palette.Color_14
Palette.Color_15
Palette.Color_16
Password_Level
Recalc_Settings
Statistics
29
30
31
32
33
34
35
Statistics.Created
Statistics.Directory
Statistics.FileName
Statistics.Last_Saved
Statistics.Last_Saved_By
Statistics.Revision_Number
Summary
05-Apr-10 05:45 PM
(A folder)
@PROPERTY.qpw
21-Feb-15 01:33 PM
Charles Cork
52
Charles M. Cork III
36 No
37 100
System
Zoom_Factor
Title, Subject,
Comments
Yes | No
10-400
Author,
Keywords,
1
2
3
4
5
6
7
A
Application
.Compatibility
Compatibility.AlternateMenuBar
Compatibility.AutoArrayWrap
Compatibility.CompatibilityMode
Compatibility.Def_Columns_Limit
Compatibility.Def_Rows_Limit
Compatibility.Def_Sheets_Limit
B
18278,256,1000000,No,No,Custom,
A:A1..B:B2,Letters,256,<QP8/9 Menu>,QPW
<QP8/9 Menu>
No
Custom
256
1000000
18278
37
8
9
10
11
12
13
Compatibility.File_Extension
Compatibility.Min_Number_Sheets
Compatibility.Range_Syntax
Compatibility.Sheet_Tab_Label
Country_Settings
Current_File
14 Display
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
Display.Clock_Display
Display.CommentMarkers
Display.Default_View
Display.Default_Zoom
Display.FormulaMarkers
Display.History_List
Display.Min_Number_Sheets
Display.Range_Syntax
Display.RealTime_Prev
Display.Sheet_Tab_Label
Display.Shortcut_Keys
Display.Show_GroupBox_As_Line
Display.Show_InputLine
Display.Show_PreSelection
Display.Show_Property_Band
Display.Show_Scroll_Indicator
Display.Show_Tool_Hint
Display.Show_Toolbar
File_Options
34
35
36
37
38
39
40
41
42
43
File_Options.AutoBack_Enabled
File_Options.AutoBack_time
File_Options.AutoLoad_File
File_Options.AutoRefreshTime
File_Options.DoRefresh
File_Options.File_Extension
File_Options.Full_Path_Titles
File_Options.QuickTemplates
File_Options.Startup_Directory
File_Options.TempDir
44
45
46
47
48
49
50
51
52
53
54
General
General.Calc-As-You-Go
General.Cell_Reference_Checker
General.Compatible_Formula_Entry
General.Compatible_Keys
General.Delay_Time
General.Direction
General.Fit-As-You-Go
General.MoveCellOnEnterKey
General.QuickType
General.Undo
QPW
256
A:A1..B:B2
Letters
$,Prefix,United States
C:\cmc\Spreadsheets\Samples\Reference\
@COMMAND.qpw
None,Yes,Yes,Yes,A..B:A1..B2,Yes,Yes,Yes,
Draft,Letters,256,Yes,Yes,Yes,100,Yes,Yes,No
None
Yes
Draft
100
Yes
Yes
256
A..B:A1..B2
Yes
Letters
Yes
Yes
Yes
Yes
Yes
Yes
C:\cmc\SpreadsheetsQPW,Yes,3,Yes,No,Yes,
C:\Users\Charles Cork\AppData\Roaming\
Corel\PerfectExpert\17\EN\
Custom QP Templates,20,No
Yes
3
20
No
QPW
Yes
Yes
C:\cmc\Spreadsheets
C:\Users\Charles Cork\AppData\Roaming\
Corel\PerfectExpert\17\EN\
Custom QP Templates
Yes,No,Yes,Yes,5000,No,No,No,Yes,Yes,Down
No
Yes
No
No
5000
Down
No
Yes
Yes
Yes
38
55 International
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
International.Currency
International.Currency_Symbol
International.Date_Format
International.Language
International.LanguageMode
International.LICS_Conversion
International.Negative
International.Placement
International.Punctuation
International.Time_Format
Macro
Macro.Macro_Redraw
Macro.Slash_Key
Macro.Startup_Macro
Title
$,Windows
Default,Prefix,Parens,Windows
Default,MM/DD/YY (MM/DD),Windows Default,Suite
Default,"English, North American (EN)",No,United
States
Windows Default
$
MM/DD/YY (MM/DD)
"English, North American (EN)"
Suite Default
No
Parens
Prefix
Windows Default
Windows Default
BothMenu Bar,\0,No
Both
Menu Bar
\0
Quattro Pro
39
40
Chapter 5
@@
@@ is a poorly documented function that is quite useful for different
purposes. We begin with an illustration (Table 5.1) of what the formulas
return in columns B, D, and F (as printed in columns C, E, and G), using the
values in A1..A4.
Table 5.1: Return values of @@ by direct reference
A
1
7
2
5
3
A1
4 A1..A2
B
7
5
A1
A1..A2
C
@@("A1")
@@("A2")
@@("A3")
@@("A4")
D
ERR
ERR
7
ERR
E
@@(A1)
@@(A2)
@@(A3)
@@(A4)
F
ERR
ERR
7
12
G
@SUM(@@(A1))
@SUM(@@(A2))
@SUM(@@(A3))
@SUM(@@(A4))
From these results, we can derive that @@ actually does two quite different things, either (1) returning the contents of a cell or block/array, or (2)
converting text coordinates into non-text coordinates:
41
1
2
3
4
5
A
5
10
15
20
25
B
A1
A3
A5
C
5
15
25
D
@@(B1..B3)
The indirect references (B1..B3) could point to cells anywhere; the cells
to which they point need not be contiguous or form a block; and there may be
any number of indirect references in the BlockOfReferences argument.
42
Comments
@@ is among the more poorly documented of QPs functions. I suspect
that its functionality has expanded over the years, but the help file was not
updated to reflect it.
Contrary to the QP help file, this text argument for @@ need not be in
a different cell. The documentation states that the argument is a single
cell address that contains another cell address or cell name that is written
as a label. So, one would not guess that the argument could be a function
that creates a text string that represents multiple cells, though it can: the
final example in the current help file on @@ shows its use in this context, but
the poor reader trying to find a function that works would be unlikely to
43
have gotten that far. Ive only stumbled on this use of @@ because of trying
everything I could imagine to get some functions to work.
Table 5.3 illustrates the significant difference between two apparently
slight variations in composing indirect references through a block of cells.
Table 5.3: The importance of structuring an indirect reference
1
2
3
4
5
A
50
40
30
20
10
B
20
25
30
35
40
C
A1
B5
D
40
10
E
@MIN(@@(C1..C2))
@MIN(@@(C1& ..& C2))
The formula in D1 returns the lesser of the values in cell A1 or cell B5; the
formula in D2 returns the least value in the block A1..B5.
@ADDRESS
@ADDRESS(Row#,Col#,<RefType,Format,Page>) has two mandatory arguments and three optional ones.
Row The Row# argument is equivalent to the blocks on the left side of the
data.
Col Column A is Col# 1, B is Col# 2, and so on.
RefType The RefType optional argument is a number from 1 to 8 that
determines which combination of absolute and relative coordinates (see
discussion at 4) will be returned. The default appears to be absolute
coordinates for page, column, and cell.
Format The Format optional argument specifies whether to use the absolute reference system (the default) or the relative system. I do not yet
see a reason for using the latter, but perhaps there is one.
Page The Page optional argument determines whether the reference will be
preceded with the name of a page or not. The page can be the default
A, B, etc., or it can be a named page, such as Data. If the named
page does not exist, the function would return an ERR.
@ADDRESS returns the coordinates as text. Some functions and commands can use those text coordinates for further automation, but for others,
the text coordinates must be converted to non-text coordinates by wrapping
the function in an @@ function.
44
@OFFSET
@OFFSET(StartCellBlock,RowOffset#,ColOffset#,<Height>,
<Width>)has five parameters, the first three of which are mandatory and
the latter two are optional.
The first three mandatory parameters set the starting point by identifying the starting cell, a row offset, and a column offset.
The StartCellBlock is typically hard-coded as a cell (often A1), but if
it is generated by some other function, if must be in the form of a block, such
as A1..A1. The block need not refer to one cell; it could refer to an entire
database such as Data:A1..Z1000, but offsets will still be measured from the
topmost, leftmost cell in the block.
Both offsets start with 0, not 1, so setting those offsets to 0 means that
the block begins in the starting cell. If you use only the three mandatory
parameters, the @OFFSET function identifies only one cell, though it does so
as a block like A1..A1.
The two optional parameters identify a block by asking for how many
rows and how many columns are considered. The minimum values for each
are 1, since a block cannot have zero width or height.
@OFFSET also returns the coordinates as text. Some functions and commands can use those text coordinates for further automation, but for others,
the text coordinates must be converted to non-text coordinates by wrapping
the function in an @@ function.
For techniques of identifying cells and blocks relative to the currently
selected cell, see page 98 below.
46
B
100
101
102
103
104
105
106
107
C
108
109
110
111
112
113
114
115
D
116
117
118
119
120
121
122
123
F2 = @OFFSET(@@(F1),4,2)
F4 = @OFFSET(@@(F1),0,2,8,1)
E
124
125
126
127
128
129
130
131
F
132
133
134
135
136
137
138
139
A1..E8
C5..C5
120
C1..C8
956
F3 = @INDEX(@@(F1),2,4)
F5 = @SUM(@@(@OFFSET(@@(F1),0,2,8,1)))
B
address
$G$5
C
col
7
D
row
5
E
contents
D4
F
type
v
G
prefix
H
format
G
@BLOCKNAME, @BLOCKNAMES
I generally prefer not to use named blocks for reasons addressed above
at iv, but many prefer them. Two functions may be useful.
If a particular block of cells has been named, @BLOCKNAME(Block) returns the name that this precise block of cells has been given. Thus, if the
user has named cells B2..C7 as Invoice, @BLOCKNAME(B2..C7) returns the
47
1
2
3
4
5
6
A1..C6 are named Invoice
E2..G3 are named Criteria
C5..D5 are named Test
Using this data, the following functions will yield the following twocolumn results:
Table 5.7: @BLOCKNAMES Return Values
Function
Block name
Invoice
@BLOCKNAMES(A1..G6) Criteria
Test
Invoice
@BLOCKNAMES(C1..E2)
Criteria
Invoice
@BLOCKNAMES(A1..C6)
Test
48
Coordinates returned
(with notebook and sheet names)
A1..C6
E2..G3
C5..D5
A1..C6
E2..G3
A1..C6
C5..D5
Chapter 6
7 above. If the numbers require special formatting, the list should contain a
function that converts the number to suitably formatted text.
@CONCATENATE(List,separated by commas) combines into a single
string of text a series of items, whether text or numbers, whether hard coded
into the list or contained in cells that are referenced in the list. It can use
cell addresses to combine the contents of those cells, as shown in Table 6.1.
Table 6.1: Joining text and block functions
A
B
C
1
28
70
50
2 Number > 50 = 4
D
6
E
42
F
53
G
63
H
24
I
83
J
9
@UPPER, @LOWER,
B
Not there
ERR
1
0
C
it is here
4
0
1
@FIND answers the question with a number (Yes) or ERR (No), you can use
the @ISERR function to distinguish. That function, however, returns 1 if the
formula causes an ERR, which means that the Subtext is not in the Text,
and 0 if it is. Since that is the reverse of what we want to know, it can be
wrapped in an @NOT function. The resulting formula yields 1 (Yes) if the
subtext is in the text, and 0 otherwise. This shows the returned values.
@IF(@ISERR(@FIELD(A1,3," ")),
@IF(@ISERR(@FIELD(A1,2," ")),@FIELD(A1,1," ")@FIELD(A1,2," "))
,@FIELD(A1,3," "))
53
@IF(@ISERR(@FIELD(A1,4," ")),
@IF(@ISERR(@FIELD(A1,3," ")),
@IF(@ISERR(@FIELD(A1,2," ")),@FIELD(A1,1," ")@FIELD(A1,2," "))
,@FIELD(A1,3," "))
,@FIELD(A1,4," "))
@IF(@ISERR(@FIELD(A1,5," ")),
@IF(@ISERR(@FIELD(A1,4," ")),
@IF(@ISERR(@FIELD(A1,3," ")),
@IF(@ISERR(@FIELD(A1,2," ")),@FIELD(A1,1," ")@FIELD(A1,2," "))
,@FIELD(A1,3," "))
,@FIELD(A1,4," "))
,@FIELD(A1,4," ")),@FIELD(A1,5," "))
(c) linking the second pseudo-column of 1s and 0s to the Row numbers; (d)
using an @MAX formula to get the highest row number with a 1 in it, which
will also be the offset location of the last space in the original text. The
beauty of QP is that all of this can be accomplished in a single function,
without placing actual data into actual columns.
The following discussion shows how we can create this function in four
phases:
(a) Since the @MID function can pluck out a single character from a text,
a series of @MID functions in a column that sequentially plucked out single
characters from the text at A1 would create the desired column. It would
start with @MID(A1,0,1), then @MID(A1,1,1), and so on. But we need
the column to stop at the last character in A1, because otherwise it will yield
an ERR. We can determine what the offset number of the last character by
@LENGTH(A1) and subtracting 1 (since the first character has an offset of 0).
If QP syntax allowed it, we could use its Block formula shorthand system
and type the invalid formula @MID(A1,1..@LENGTH(A1),1) to have QP
generate the column, but it does not allow the @LENGTH(A1) syntax to
specify an array of numbers. However, QP does allow the @ROW function to
supply the row numbers of a block of cells, and we can create a block of cells
large enough to hold precisely each single character in A1. The easiest block
of cells would be:
@OFFSET(A1,0,0,@LENGTH(A1),1)
If the text in A1 had 100 characters, this would return the block A1..A100.
(The reader need not worry that this overlaps the source cell and causes the
circularity indicator in the application bar at the bottom to display, since
this will all be combined in a single function that could be placed anywhere.
This function could, however, be written to create the block anywhere.) Since
@OFFSET returns the block as text, and well need to turn it into coordinates,
it must be wrapped in an @@ function, as follows:
@@(@OFFSET(A1,0,0,@LENGTH(A1),1))
That formula (which cant stand alonesee the section on @@ at 41) is now
ready to wrap in an @ROW function:
@ROW(@@(@OFFSET(A1,0,0,@LENGTH(A1),1)))
This function will create a column of numbers, starting with 1 and going
to the number of characters in A1. It is now ready to plug into the @MID
function after one change. We need to subtract 1 from each of those row
numbers to deal with the offset in @MID starts with 0. As such, the final
formula that generates a column of single characters from the text in A1 is
this:
@MID(A1,@ROW(@@(@OFFSET(A1,0,0,@LENGTH(A1),1)))
-1,1)
Interestingly, QP automatically wraps this formula in an @ARRAY function,
so the final product of this stage would look like this if we stopped here:
55
@ARRAY(@MID(A1,@ROW(@@(@OFFSET(A1,0,0,
@LENGTH(A1),1)))-1,1))
(b) The rest of the steps are easier to build on, using QPs Block formulas.
The last formula can be modified to yield a parallel column of 1s and 0s
depending on whether the column of characters contains a space (1) or not
(0), by adding the equation at the end:
@ARRAY(@MID(A1,@ROW(@@(@OFFSET(A1,0,0,
@LENGTH(A1),1)))-1,1)=" ")
(c) Another column can be created that multiplies the last column of 1s
and 0s by the row number, so that we get instead a column of 0s and row
numbers.
@ARRAY((@MID(A1,@ROW(@@(@OFFSET(A1,0,0,
@LENGTH(A1),1)))-1,1)=" ")
*(@ROW(@@(@OFFSET(A1,0,0,@LENGTH(A1),1)))-1))
This formula multiplies the last formula by the formula for row numbers
above.
(d) We can therefore get the offset of the last space by applying the @MAX
function to the last formula. It simply replaces the @ARRAY component.
@MAX((@MID(A1,@ROW(@@(@OFFSET(A1,0,0,
@LENGTH(A1),1)))-1,1)=" ")
*(@ROW(@@(@OFFSET(A1,0,0,@LENGTH(A1),1)))-1))
Since we want the text to the right of that, we want a number of characters
equal to the total number of characters, less the number found by the last
formula, and less 1 (since otherwise our text would start with that space).
So, the final formula works out to:
@RIGHT(A1,@LENGTH(A1)@MAX((@MID(A1,@ROW(@@(@OFFSET(A1,0,0,
@LENGTH(A1),1)))-1,1)=" ")
*(@ROW(@@(@OFFSET(A1,0,0,@LENGTH(A1),1)))-1))
-1)
56
Chapter 7
Math Functions
QP has a tremendous number of functions for advanced math, geometry,
statistics, engineering, finance, and more, but this text will focus only on
more basic functions.
57
B
2008
2008
2009
2009
2010
2011
3
6
7
4
9
6
E
2008
2009
2010
2011
2012
2013
9
11
9
6
0
0
E1 = @SUMIF($A$1..$B$6,D1,$B$1..$B$6)
copied to cells E2..E6
E1 is equivalent to @SUM((B1..B6)*(A1..A6=D1)).
1
1
-1
-1
2
2
-2
-2
3
3
-3
-3
E
Avg
2
1.5
-2
-1.5
F
PureAvg
2
2
-2
-2
G
Max
3
3
-1
0
H
Min
1
0
-3
-3
I
Puremax
3
3
-1
-1
J
Puremin
1
1
-3
-3
Functions indicated in E1..J1 were typed into E2..J2, with a2..d2 as the
argument, then copied, and then pasted below.
5
10
15
20
25
30
B
Ascending
1
1
2
3
4
5
6
B3 = @RANK($A3,$A$3..$A$8,B$2)
C
Descending
0
6
5
4
3
2
1
copied to B3..C8
3
1234.568
2
1234.57
1
1234.6
0
1235
B2 = @ROUND($A$2,B1)
F
-1
1240
G
-2
1200
H
-3
1000
copied to C2..H2
-1
-1
-1
-1
B
-0.75
0
-1
-0.7
A2 = @INT(A1)
C
-0.5
0
-1
-0.5
D
-0.25
0
0
-0.2
E
0
0
0
0
F
0.25
0
0
0.2
A3 = @ROUND(A1,0)
G
0.5
0
1
0.5
H
0.75
0
1
0.7
I
1
1
1
1
A4 = @TRUNC(A1,1)
60
+B1+B3+B5+B7+B9+B11
For small numbers of entries, this is practical. For larger numbers, this is
impractical.
61
1
2
3
4
5
6
7
8
9
10
11
12
13
A
Jan
Check#
Feb
Check#
Mar
Chk#
Apr
Chk#
May
Chk#
Jun
Chk#
B
123.45
1002
132.54
1004
213.45
1006
231.54
1008
321.45
1010
312.54
1012
?
Method 2. Using QPs array formulas and the fact that the numbers to be
summed are on odd rows, this formula works:
@SUM(B1..B12*@ISODD(@ROW(B1..B12)))
If instead we were summing the even rows, the formula would be:
@SUM(B1..B12*@ISEVEN(@ROW(B1..B12)))
Method 3. A variation on this approach that does not depend on whether
the target data is on odd or even rows is this:
@SUM((B1..B12)*(@MOD(@ROW(B1..B12),2)
=@MOD(@ROW(B1),2)))
This last uses @MOD to set the test for a row to add based on whether the
row number, divided by 2, has the same remainder as the first rows number,
divided by 2. It thus counts values in every other row, beginning with the
first row.
However, the following formula in A11 will allow other rows to be inserted
above the function, and it will sum the numbers from A1 to the cell above
this function.
@SUM(@@(@OFFSET(A1,0,0,@ROW-1,1)))
But what if a row is inserted above A1? Doing that has the effect of
changing the A1 in this formula automatically to A2, and because the size
of the block created by @OFFSET doesnt change, it defines a block that
includes this formula. So, instead, we need a function that sets the A1 cell
in concrete, so to speak. Here are two ways of doing so:
@SUM(@@(@OFFSET(@@("a1..a1"),0,0,@ROW-1,1)))
@SUM(@@(@CONCATENATE("a1..a",@ROW-1)))
Both create the coordinates of a block entered in text format and anchored in
cell A1, which are converted to coordinates by the @@ function before being
summed by @SUM.
But what if the user might insert a column before that column? In that
case, the functions that link only to cell A1 would give false answers. Instead,
we need a function that links always to the first cell in the column in which
the function appears. Unfortunately, @COLUMN doesnt work the same way
as @ROW for reasons that I do not understand, so the cases are not parallel.
But @CELL("col") gives the same result, so to get the current column for
a given function, use:
@CELL("col",c(0)r(1))
Parenthetically, instead of c(0)r(1), we could type the coordinates of
any cell in the same column, but using this method allows us to put the
formula into any cell anywhere, and QP will automatically replace it with
the cell below the one in which it is pasted. Further parenthetically, I
prefer c(0)r(1) over c(0)r(0), because its use would cause the circularity
indicator to activate.
The last function gives us a column number that will update whenever
columns are inserted or deleted. The function @INDEXTOLETTER will convert
that column number to a letter, but we need to subtract 1 from the column
number because @CELL returns 1 for column A, but @INDEXTOLETTER converts 0 to A, 1 to B, etc.
@INDEXTOLETTER(@CELL("col",c(0)r(1))-1)
We now have each of the components to create the coordinates of the desired
block:
@CONCATENATE(@INDEXTOLETTER(@CELL("col",
c(0)r(1))-1),"1..",@INDEXTOLETTER(@CELL("col",
c(0)r(1))-1),@ROW-1)
When wrapped with @SUM and @@, this formula sums the contents of the
current column, from the top to the cell above the function, no matter how
many rows or columns are inserted or deleted.
63
@SUM(@@(@CONCATENATE(@INDEXTOLETTER(@CELL("col",
c(0)r(1))-1),"1..",@INDEXTOLETTER(@CELL("col",
c(0)r(1))-1),@ROW-1)))
64
Chapter 8
65
@DATE, @YEAR,
1
2
3
4
5
6
7
8
9
10
11
12
13
B
2/12/2015
Thu
Thursday
3
7
Feb
February
28
16
42063
1
0
43
322
C
Day of the week (short)
Day of the week (full)
Day of week, 0=Mon to 6=Sunday
Week of the year, from 1 to 53
Month (short)
Month (full)
Number of days in this month
Number of days left in the month
The last day (integer) in the month
Quarter of the year (1 to 4)
Whether a leap year (1=yes; 0=no)
Day of the year (1 to 366)
Days left in the year
B2 = @DATEINFO($B$1,A2)
copied to B3..B14
into cells, they can be joined by @CONCATENATE. But the function returns
nothing inside @CONCATENATE. Perhaps that quirk is related to the quirk
that QP wraps @DATEINFO in an @ARRAY function for reasons that are
opaque to me. However, in lieu of @CONCATENATE, these items can be joined
in an @ARRAY function using ampersands to combine them.
Thus, in this example,
@ARRAY("A "&@DATEINFO(A1,2)&" in "&@DATEINFO(A1,6))
returns: A Thursday in February.
Years
Months
Days
Days, disregarding months and years
Months, disregarding years
Days, disregarding years
The user should be cautious in using it, because it can express the
difference between two numbers in unexpectedly negative terms, as Table
8.3s illustration of three days around the 14th anniversary of a start date
shows:
Table 8.3: @DATEDIF Return Values
1
2
3
4
A
Start Date
End Date #1
End Date #2
End Date #3
B
8/8/2000
8/7/2014
8/8/2014
8/9/2014
C
y
13
14
14
C2= @DATEDIF($B$1,$B2,C$1)
D
m
168
168
168
E
d
5112
5113
5114
F
md
-1
0
1
G
ym
11
0
0
H
yd
-2
-1
0
copied to C2..H4
68
69
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
A
03/27/13
05/18/13
08/23/13
08/24/13
09/30/13
10/03/13
12/11/13
12/12/13
02/03/14
02/20/14
02/26/14
03/16/14
05/25/14
07/16/14
08/23/14
10/13/14
11/08/14
12/08/14
02/06/15
02/11/15
B
71
71
68
28
40
47
30
30
30
46
41
59
58
65
26
48
54
58
35
43
C
1
2
3
4
5
6
7
8
9
10
11
12
D
2013
0
0
71
0
71
0
0
96
40
47
0
60
E
2014
0
117
59
0
58
0
65
26
0
48
54
58
F
2015
0
78
0
0
0
0
0
0
0
0
0
0
Well also use @YEAR to compare the dates in A1..A20 with D1. As we
copy the formula to the rest of the grid, well want it to vary with the column,
but not the row. We do that by specifying the comparison value as D$1. But
as weve noted, @YEAR returns numbers 1900 less than our calendar years,
so well need to subtract 1900 from the value in D1.
And as we copy the formulas around, well need to make sure that they
refer to the same data in columns A and B, so those will need to be made
absolute. Accordingly, the formula to put into cell D2 is:
@SUM(($B$1..$B$20)
*(@MONTH($A$1..$A$20)=$C2)
*(@YEAR($A$1..$A$20)=D$1-1900))
Copying that formula to D2..F13 puts the formulas in those cells that correctly return the numbers set out above.
Caution. If Columns A and B had text headers, like Date and Number
in A1 and B1, the @YEAR function would ERR. In that case, all of the blocks
would need to be changed to go from row 2 to row 20.
70
Site
Site A
Site B
Site C
Wk
1
1
1
Day
4
5
6
72
E
Cur. Month
2015
3
03/03/15
03/04/15
03/05/15
F
Next Month
2015
4
04/07/15
04/01/15
04/02/15
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
03/10/15
03/10/15
03/11/15
03/11/15
03/12/15
03/12/15
Site D
Site E
Site F
Site G
Site H
Site I
Site J
Site K
Site L
Site M
Site N
Site O
Site P
Site Q
Site R
Site S
Site T
Site U
Site V
Site W
Site X
2
2
2
2
2
2
3
3
3
3
3
3
3
4
4
4
4
4
4
L
L
4
4
5
5
6
6
3
4
4
5
6
6
7
3
4
5
5
5
6
3
4
03/10/15
03/10/15
03/11/15
03/11/15
03/12/15
03/12/15
03/16/15
03/17/15
03/17/15
03/18/15
03/19/15
03/19/15
03/20/15
03/23/15
03/24/15
03/25/15
03/25/15
03/25/15
03/26/15
03/30/15
03/31/15
04/14/15
04/14/15
04/08/15
04/08/15
04/09/15
04/09/15
04/20/15
04/21/15
04/21/15
04/15/15
04/16/15
04/16/15
04/17/15
04/27/15
04/28/15
04/22/15
04/22/15
04/22/15
04/23/15
04/27/15
04/28/15
The cells highlighted in blue contain the functions that, when copied
to the yellow cells, operate the sheet. @YEAR(@TODAY)+1900 is in E2,
@MONTH(@TODAY) in E3, @IF(E3=12,E2+1,E2) in F2, and @IF(E3=12,1,
E3+1) in F3. E4 contains @NWKDAY($C4,$D4,E$3,E$2), which is then
copied to E4..F25. E26 contains @LWKDAY($D26,E$3,E$2), which is then
copied to E26..F27.
A4 contains the formula that yields the desired results:
@IF(@SUM((E4..F4>@TODAY)*(E4..F4<=@TODAY+7)),
@SUM((E4..F4)*(E4..F4>@TODAY)*(E4..F4<=@TODAY+7)),
"")
which is then copied to A5..A27. It displays delivery dates in the next seven
days, if any, and otherwise displays a blank. If there is a date within the
next 7 days in cells E4 or F4, the first @SUM function will return 1, otherwise
it will return 0. If the latter, the function returns a blank, but if the former,
the second @SUM function returns that date.
73
Solution 1: @NWKDAY
This solution uses @NWKDAY to determine the date of the 5th Wednesday
in a given month. If there are 5 Wednesdays, it returns a date; if not, it
returns ERR. Since no month has less than 28 days nor more than 31, there
will either be 4 or 5 Wednesdays in the month, so @ISERR will help us decide.
This function returns the answer.
@IF(@ISERR(@NWKDAY(5,5,B1,A1)),"4 Wednesdays",
"5 Wednesdays")
The first 5 seeks to find the 5th instance of the second 5, which is Wednesday
on the scale of Saturday=1 to Friday=7. If there is no 5th Wednesday, ERR
results, and this function returns 4 Wednesdays; otherwise it returns 5
Wednesdays.
@OFFSET(A1,0,0,LastDay-FirstDay+1,1)
or
@OFFSET(A1,0,0,@EMNTH(@DATE(A1,B1,1))
-@DATE(A1,B1,1)+1,1)
In the case of April, that column would be A1..A30. For 31-day
months, it would be A1..A31. These would be text. Converting the
text block to non-text coordinates requires wrapping it in @@.
@@(@OFFSET(A1,0,0,@EMNTH(@DATE(A1,B1,1))
-@DATE(A1,B1,1)+1,1))
(b) Wrapping the resulting function inside @ROW() yields an array
numbering 1..30 in a column of 30 cells. QP automatically wraps
that in an @ARRAY() function.
@ARRAY(@ROW(@@(@OFFSET(A1,0,0,
@EMNTH(@DATE(A1,B1,1))-@DATE(A1,B1,1)+1,1))))
(c) Converting that array to the array of date numbers in the current
month, we add the FirstDay-1 to each.
@ARRAY(@ROW(@@(@OFFSET(A1,0,0,
@EMNTH(@DATE(A1,B1,1))-@DATE(A1,B1,1)+1,1)))
+@DATE(A1,B1,1)-1)
That gives us an array of 30 days matching the 30 days in this
month.
(d) Applying @MOD(#,7) to each number in the array, each becomes
a number between 0 and 6 (Saturday through Friday).
@ARRAY(@MOD(@ROW(@@(@OFFSET(A1,0,0,
@EMNTH(@DATE(A1,B1,1))-@DATE(A1,B1,1)+1,1)))
+@DATE(A1,B1,1)-1,7))
(e) Testing whether each is equal to 4 (Wednesdays) yields a series of
0s and 1s.
@ARRAY(@MOD(@ROW(@@(@OFFSET(A1,0,0,
@EMNTH(@DATE(A1,B1,1))-@DATE(A1,B1,1)+1,1)))
+@DATE(A1,B1,1)-1,7)=4)
4. Replacing the @ARRAY() wrapper with @SUM() totals the number of
1s, which happens to total 5 for this April of 2015.
@SUM(@MOD(@ROW(@@(@OFFSET(A1,0,0,
(@EMNTH(@DATE(A1,B1,1))-@DATE(A1,B1,1))+1,1)))
+@DATE(A1,B1,1)-1,7)=4)
Upon changing the year in A1 and month in B1, this function returns
the correct number of Wednesdays for the chosen year and month.
+Date#+@MOD(7+Weekday#-@WEEKDAY(Date#),7)
so in the case of the first Wednesday in April of 2015:
+@DATE(2015,4,1)
+@MOD(7+4-@WEEKDAY(@DATE(2015,4,1)),7)
This starts the count at 1, and the question will be how many more exist
between the first date and the end date. This turns out to be the integer
portion of (the difference between the end date and the start date) divided
by 7. For months, it will be safe to use @INT, since there are going to be at
least some additional Wednesdays, but if we want to use this formula on
different dates in which there might not be a Wednesday, @ROUNDDOWN will
give the correct result.
For shorthand, let us store the Year# (2015) in A1, the Month# (4) in A2,
the Weekday# (4) in A3. In A4, we place the first day of the month, using
the function @DATE(A1,A2,1). In A5, we place the last day of the month,
using the function @EMNTH(A4). This formula returns the number of the
Weekday# in that calendar month:
+1+@ROUNDDOWN((A5-A4+@MOD(7+A3-@WEEKDAY(A4),7)))/7)
76
Chapter 9
Result
@STRING(1234.567,2)
1234.57
@FIXED(1234.567)
1,234.57
@FIXED(1234.567,2)
1,234.57
@FIXED(1234.567,2,1)
1234.57
Result
@DOLLARTEXT(12.34,1)
Twelve
@DOLLARTEXT(12.34,2)
Twelve Dollars
@DOLLARTEXT(12.34,3)
@DOLLARTEXT(12.34,4)
@DOLLARTEXT(12.34,5)
Result
@FRACTION(1234.567,2)
1234 1/2
@FRACTION(1234.567,3)
1234 2/3
@FRACTION(1234.567,4)
1234 1/2
@FRACTION(1234.567,5)
1234 3/5
@FRACTION(1234.567,6)
1234 1/2
@FRACTION(1234.567,7)
1234 4/7
@CHAR(Number) converts a number from 1 to 255 into the ANSI character associated with that number. Thus, @CHAR(32) is a space; @CHAR(34)
is a double-quote. The converse conversion function is @CODE(Character).
80
the number to be padded is in cell A1 and the number of characters the label
is in cell B1. These formulas work:
@REPEAT("0",B1-@LENGTH(@STRING(A1,0)))&@STRING(A1,0)
@SUBSTITUTE(@SETSTRING(@STRING(A1,0),B1,2)," ","0")
@RIGHT("0000000000000000000"&@STRING(A1,0),B1)
@RIGHT(@CONCATENATE(@REPEAT("0",B1),A1),B1)
And, of course, the user can create a custom numeric format. See the
discussion in WPU 30801.
83
+@TIME-
July 9, 2010
01-25-2014
01-25-2014
2010-07-08 00:00:00
@DATEVALUE(@CONCATENATE(@SUBSTITUTE
(@FIELD(A1,2," "),",",""),"-",@LEFT(@FIELD(A1,1," "),3),
"-",@FIELD(A1,3," ")))
@DATEVALUE(@CONCATENATE(@FIELD(@FIELD(A1,2,","),3,
" "),"-",@LEFT(@FIELD(@FIELD(A1,2,","),2," "),3),"-",
@RIGHT(@FIELD(A1,3,","),4)))
@DATE(@VALUE(@FIELD(A1,3,"-")),
@VALUE(@FIELD(A1,1,"-")),@VALUE(@FIELD(A1,2,"-")))
@DATEVALUE(@CONCATENATE(@FIELD(A1,1,"-"),"/",
@FIELD(A1,2,"-"),"/",@FIELD(A1,3,"-")))
@DATEVALUE((@FIELD(@FIELD(C1,1," "),2,"-")&"/"
&@FIELD(@FIELD(C1,1," "),3,"-")&"/"
&@FIELD(@FIELD(C1,1," "),1,"-")))
84
created in a and b above only if the value in the cell is less than
100, then under First Term choose This Cell, under Operator
choose <, and under Second Term choose Value and enter
100 to the right.
(d) This creates a formatting rule, and you are now ready to add the
formatting rule to the Rules Box by clicking Add under the Rules
Box.
(e) You can add more rules (which might be useful if you made the
formatting on conditions that might not apply to some cells). First,
the last step will cause the dialog to select the newly added Rule,
so deselect it to create another rule; otherwise, QP thinks that
you are trying to modify the last rule. Then clear out unwanted
conditions and format codes, and then repeat steps a to d.
You are now ready to add this as a custom format.
6. Add this as a Custom Format by clicking [OK]. (That was easy.) Youre
now ready to select some cells and then use steps 1 and 2 to select this
format.
87
88
Chapter 10
spreadsheet, and in cells below those headings, one or more values would
be entered (the criteria), and these would have the effect of narrowing the
search through the database to rows that match the criteria. That approach
has merit, merit which increases with the complexity of the criteria, but it is
useful primarily for a small number of more narrowly focused applications
of QP, not for the general purposes that this guide seeks to address. (The
approach is a close cousin of the notebook query, which is considerably more
useful for ordinary purposes. See page 200 for more.)
The following functions get useful information about and from a database.
91
1
2
3
4
5
6
A
2008
2008
2009
2009
2010
2011
B
3
6
7
4
9
6
C
5
1
D
@COUNTIF(A1..A6<=2010)
@COUNTIF(A1..A6>2010)
E
2
4
F
@COUNTIF(B1..B6<=5)
@COUNTIF(B1..B6>5)
@IF(@lASTCELLVALUE(A1..A10)="","a",
@LASTCELLVALUE(A1..A10))
This will yield an ERR if any values are in A1..A10, but it correctly returns
a otherwise. One workaround is:
@IF(@ROWS(A1..A10)=@COUNTBLANK(A1..A10),"a",
@LASTCELLVALUE(A1..A10))
The function also returns an ERR when used as an argument in an
@DATEDIF function. Thus, comparing a start date in A1 with the last date
entered in the column below it, the function
@DATEDIF(A1,@LASTCELLVALUE(A2..A10),"y")
returns ERR, though
@DATEDIF(A1,@MAX(A2..A10),"y")
does not. A workaround is to put the @LASTCELLVALUE function in another
cell and compare the start date with that cell.
10
20
30
40
50
60
65
70
75
80
85
90
95
100
B
F
F
F
F
F
DD
CC
BB
AA
A+
Grades
70
82
90
92
97
100
104
E3 = @VLOOKUP($D3,$A$1..$B$14,1,E$2)
CBAAA
A+
A+
F
Match
1
CERR
AERR
ERR
A+
ERR
copied to E3..F9
95
1
2
3
4
5
6
7
A
Month
Jan
Feb
Mar
Apr
May
Jun
B
Date
01/01/15
01/08/15
01/15/15
01/22/15
01/29/15
02/05/15
C
Name
Alan
Betty
Charlie
Doris
Ed
Fran
D
Amount
$70.58
$557.78
$935.92
$798.28
$342.29
$780.72
E
@LOOKUP(Doris,C1..C7,D1..D7) *
@VLOOKUP(Apr,A1..D7,3,0)
@HLOOKUP(Amount,A1..D7,4,0)
@VHLOOKUP(Apr,Amount,A1..D7) *
@INDEX(A1..D7,3,4)
@XINDEX(A1..D7,Amount,Apr) *
@CELLINDEX("contents",A1..D7,3,4)
96
B
10
20
30
40
50
60
65
70
75
80
85
90
95
100
F
F
F
F
F
DD
CC
BB
AA
A+
Grades
70
82
90
92
97
100
104
E3 = @MATCH($D3,$A$1..$A$14,1,E$2)
E
Match
1
7
9
11
11
12
13
13
0
7
ERR
11
ERR
ERR
13
ERR
copied to E3..F9
Cautions
Using these functions in macros. As noted below (page 122), when
used in macros, R1C1 notation requires brackets in order to make references
relative to the active cell rather than to the cell containing the current macro
command. Therefore, for purposes of these exercises, use []c(0)r(0), not
c(0)r(0). And as noted above (page 90), when used in macros, @ROW
and @COLUMN return row and column numbers for the cell in which the
macro command occurs, not for the presently active cell. To deal with this
problem in macros, specify the block to which the function relates: e.g.,
@ROW([]c(0)r(0)).
98
99
(first) argument in a wrapping @OFFSET function; it would have to be converted to A7..A7 and wrapped in @@ to be used as the base for @OFFSET.
Second, after it is initially entered, it does not update if rows or columns are
inserted, and thus it cannot be relied upon if insertions could happen.
We can, however, finesse it by using the @ROWS function to convert the
basic block into a number of rows that can be used for the second argument
in an @OFFSET function.
@ROWS(@@(@OFFSET(@@("A1..A1"),0,0,@ROW,@COLUMN)))
returns the number of rows in the basic block. Because @ROWS starts with 1,
rather than 0, we will need to subtract 1 from its result to get the correct
offset:
@ROWS(@@(@OFFSET(@@("A1..A1"),0,0,@ROW,
@COLUMN)))-1
Based on this background, the following function will always give us the
coordinates of the cell in the A column in the same row as this function:
@OFFSET(@@("A1..A1"),
@ROWS(@@(@OFFSET(@@("A1..A1"),0,0,
@ROW,@COLUMN)))-1,0)
This is the best way I know. By increasing the final 0 to 1, 2, etc., it would
return the parallel values in columns B, C, etc.
100
The beauty of this function is that it always refers to the cell to the left of the
function, even if rows are inserted anywhere between the left edge and the
column with the function in it. By modifying the offsets in this formula (the
-1 in the RowOffset# (second) argument and the -2 in the ColumnOffset#
(third) argument, the function can refer to the address of any cell that has a
defined position relative to the cell containing the formula.
102
@@(@ADDRESS(@CELL("row",D6),1))
That function can be created without knowing the cell, again, by this:
@@(@ADDRESS(@CELL("row",c(0)r(0)),1))
How to identify the first blank row and cell in a column of data
The complexity of the formula for determining the first blank cell depends
on how well-formed the database is. In the best-formed database that begins
with entries in the top cell of a column and that continues without gap until
the last entry in the column, the formula is at its simplest. If the column is
A on sheet A, the formula
@COUNT(A:A)
returns the number of non-blank cells in the column, which is also the
offset number for the first blank cell. That is, if there were no entries
at all in the column, the number would be 0, and to find the first blank
103
cell, @OFFSET(A1,0,0) would return the cell A1.. If there were only a
header in the column, the number would be 1, and @OFFSET(A1,1,0)
would return the cell A2. If there are items (header and data) in the column,
@OFFSET(A1,100,0) would return the cell A101. We can deduce that the
formula for the first blank cell in a well-formed column is:
@OFFSET(A1,@COUNT(A:A),0)
If one feels squeamish about the A:A notation, A:A1..A10000 will work if
you will always have fewer than 10,000 entries:
@OFFSET(A1,@COUNT(A:A1..A10000),0)
One must use such notation if one starts below the first row. Thus, if the
database begins at row 5, the formula would be:
@OFFSET(A5,@COUNT(A:A5..A10000),0)
Since the parallel @ADDRESS function starts counting rows and columns
from 1 rather than 0, the comparable functions would be:
@ADDRESS(@COUNT(A:A)+1,1)
@ADDRESS(@COUNT(A:A1..A10000)+1,1)
@ADDRESS(4+@COUNT(A:A5..A10000)+1,1)
The last function requires adding 4 at the start to account for the four row
gap before the database starts on row 5.
All of that assumes that there are no gaps in the data, and that there are
no stray entries below the data. @COUNT is a blunt instrument, though, and
if there are gaps or strays, it will easily yield a number other than the first
blank row below the data. A more complex formula is necessary to find the
first blank row after the last entry. We will need to identify the last entry
and determine its row, which will be the offset value of the first blank row
after the data. An initial try would be this block formula:
@MAX((A1..A10000<>"")*@ROW(A1..A10000))
@ROW returns row numbers, and the formula seeks to determine which cells
in A1..A10000 are not blank (<>""). But as the review of tests for blank
cells at page 32 shows, this would treat as blank cells that contain 0 and
functions that return an empty string, which would not typically be the
goal in looking for the first blank cell after all data. That study showed
that the only function that treats those as non-blanks and that works on
arrays is @ISBLANK, so the function needs to incorporate it. Were looking for
non-blank cells, so we need the opposite of @ISBLANK. Wrapping @ISBLANK
in @NOT does not produce an array, but an equation does. The formula, then,
for the first blank row after the last entry is:
@MAX((@ISBLANK(A1..A10000)=0)*@ROW(A1..A10000))
The formula that gets the coordinates for that cell, therefore, is:
@OFFSET(A1,@MAX((@ISBLANK(A1..A10000)=0)
@ROW(A1..A10000)),0)
104
How to count the non-blank cells in the index column of a given block
Assume that were given the coordinates of a block in cell A1, a block
which could be almost anywhere in the spreadsheet. The leftmost column of
the block is an index column. We want to know how many items are in the
block. For such a block, this formula should do:
@COUNT(@@(@OFFSET(@@(A1),0,0,10000,1)))
This formula will not work if it appears in one of the last 10,000 rows of the
105
spreadsheet, which by default has one million rows. If it is needed that far
down, an adjustment of the 10000 argument in the formula should work.
added. (This arose in the context of setting up a graph that would show only
the last five values.)
Table 10.5: Getting the last five values in a column
1
2
3
4
5
6
7
8
9
10
A
0.226
0.417
06.79
0.133
0.656
0.073
0.767
0.052
0.502
B
0.656
0.073
0.767
0.052
0.502
C
@INDEX($A:$A$1..$A$1000,0,@COUNT($A:$A$1..$A$1000)-5)
@INDEX($A:$A$1..$A$1000,0,@COUNT($A:$A$1..$A$1000)-4)
@INDEX($A:$A$1..$A$1000,0,@COUNT($A:$A$1..$A$1000)-3)
@INDEX($A:$A$1..$A$1000,0,@COUNT($A:$A$1..$A$1000)-2)
@INDEX($A:$A$1..$A$1000,0,@COUNT($A:$A$1..$A$1000)-1)
Formulas for returning the last five entries in the A column, as entries are added.
The formulas in B1..B5 are shown in C1..C5, and they will recalculate
as entries are added to the bottom of column A. @INDEX returns the value
from the A column, with the row offset determined by subtracting from the
total number of entries (determined by @COUNT) the numbers 5 through 1.
Adding another number in A10 would have the result of moving the results
in B1..B5 up one cell, and the content in A10 would then appear in B5.
Another user wanted to know how to sum the last five values in a column
to which numbers would be added. The address for the last five rows in the
column would be this:
@OFFSET(A1,@COUNT(A1..A1000)-5,0,1)
and thus the function that sums the last five entries in the column is this:
@SUM(@@(@OFFSET(A1,@COUNT(A1..A1000)-5,0,5,1)))
If we know the block in advance, we can see if the three numbers that
@CELLPOINTER gives equal the sheet number, are greater than or equal to
the leftmost column number, less than or equal to the right column number,
and likewise for the row numbers. Here, lets say that the block is A:A1..C4.
The Sheet is 1, columns are 1, 2, and 3; rows are 1, 2, 3, 4. This formula will
return 1 if the cellpointer is in the block, 0 if it is not:
@CELLPOINTER("sheet")=1
#AND#@CELLPOINTER("col")>=1
#AND#@CELLPOINTER("col")<=3
#AND#@CELLPOINTER("row")>=1
#AND#@CELLPOINTER("row")<=4
which can be wrapped with an @IF test to put the result into English:
@IF(@CELLPOINTER("sheet")=1
#AND#@CELLPOINTER("col")>=1
#AND#@CELLPOINTER("col")<=3
#AND#@CELLPOINTER("row")>=1
#AND#@CELLPOINTER("row")<=4,"in block",
"not in block")
But what if we want to be able to code a function and put a block into
it without first calculating all of the sheet, column, and row numbers? We
can use @CELL to get the sheet, column and row numbers for the first cell in
the block, and we can use @ROWS and @COLS to get the number of rows and
columns in the block so as to calculate the last row and column numbers,
and then substitute those into the tests above. But that is cumbersome.
There is, however a more elegant way to determine whether the cellpointer is in the range of columns and rows of a block, using @MATCH with
@ROW and @COLUMN. Specifically, the cellpointer will be in the same columns
as the block if this function returns a number:
@MATCH(@CELLPOINTER("col"),@COLUMN(Block),0)
If it returns ERR, the cellpointer is not in the block. Likewise, the cellpointer
is in the same rows as the block if this function returns a number:
@MATCH(@CELLPOINTER("row"),@ROW(Block),0)
In order to turn these results so that the return 1 if the cellpointer is in the
range and 0 otherwise, they need to be wrapped in @NOT(@ISERR()).
Pulling all three tests together, this formula works:
@CELL("sheet",Block)=@CELLPOINTER("sheet")
#AND#@NOT(@ISERR(@MATCH(@CELLPOINTER("col"),
@COLUMN(Block),0)+@MATCH(@CELLPOINTER("row"),
@ROW(Block),0)))
And to wrap it in an @IF formula:
108
@IF(@CELL("sheet",Block)=@CELLPOINTER("sheet")
#AND#@NOT(@ISERR(@MATCH(@CELLPOINTER("col"),
@COLUMN(Block),0)+@MATCH(@CELLPOINTER("row"),
@ROW(Block),0))),"not in block","in block")
109
typed into the cell, the function recalculates and returns information from
the appropriate row.
@MATCH is used to get the row offset in the database, and @INDEX can
then easily return data from that row by column. As long as the item is
unique, this method is straightforward. If the item is not unique, however,
this method returns only the first match. That may suffice, but if not, the
complications are explored in the next applications.
Table 10.6 contains a database of random data in A1..C7. We want to
type a name in E2, and have QP show the row in the database in E5..G5.
Table 10.6: Getting information from a database with @MATCH
1
2
3
4
5
6
7
A
Name
Eddie
Betty
Dotty
Eddie
Fannie
Ginny
B
Date
06/18/14
02/10/10
11/02/09
07/03/11
01/12/12
10/30/10
C
Score
71
72
73
73
73
81
E
Search For
Dotty
Name
Dotty
Date
11/02/09
Score
73
they do require their own set-up and manual operation, even if that is made
easier by macros. The QuickFilter can also be used to get useful information.
Each requires manual operation.
In this section, we want to do it with @Functions, which can be used, but
it requires some careful work.
We will use random data (4 people, 3 instances each, randomly arranged)
as shown in A1..B13 of Table 10.7. There are at least three strategies for
getting matches beyond the first one.
One method is to restrict the block that @MATCH searches in each row,
so as to exclude prior matches.
Another is to create a parallel array of values in which each row has a
unique value, but that the values for rows that should match will have
a particular quality that allows them, and only them, to be found by
@functions.
A third is to create a parallel array of values in which the values
increment (from 0 to 1, then 2, etc.) only when a match occurs, in
which case, @MATCH for successive numbers starting with 1 will return
the desired rows.
All of these methods require the use of helper columns. These are
columns in which some intermediate calculations are done, before the functions return the desired data. Aesthetically, they may not be very pleasing,
but if so, they can be hidden. To hide columns, select the column headers
(like C, D, etc.) to be hidden and right click, and in the popup menu, select
Hide. (To reverse the operation, select the columns on both sides of the
hidden columns, right click, and select Reveal.)
You will also need to decide how many rows of functions that you want to
set in your field for results. We dont know in advance whether any or all of
the items in the column to be searched will match. The only way to be sure
to get all matches is to have a number of rows of functions equal to the rows
of the data itself. For most purposes, you will be able to make do with less.
on the row specified in D2, but if D2 is ERR, and @IF function causes it to
be blank. The same is true for the function in G2, which returns the score
on the row specified in D2. We can then copy the functions in D2..G2 down
as long as we want. Here, they are copied down to row 5.
Table 10.7: Finding matches by successively changing the block to be searched
1
2
3
4
5
6
7
8
9
10
11
12
13
A
Name
Dotty
Dotty
Charlie
Andy
Dotty
Andy
Betty
Charlie
Andy
Betty
Betty
Charlie
B
Score
95
73
73
89
95
71
82
88
99
72
73
91
C
Andy
1
2
3
4
D
5
7
10
ERR
E
a1..a1000
a6..a1000
a8..a1000
a11..a1000
ERR
F
Name
Andy
Andy
Andy
G
Score
89
71
99
D2.
This is complicated, but it works. Change the name in C1 to any of the
other three names, and the data in F2..G5 change for that person.
1
2
3
4
5
6
7
8
9
10
11
12
13
A
Name
Dotty
Dotty
Charlie
Andy
Dotty
Andy
Betty
Charlie
Andy
Betty
Betty
Charlie
B
Score
95
73
73
89
95
71
82
88
99
72
73
91
C
0.500
0.333
0.250
1.200
0.167
1.143
0.125
0.111
1.100
0.091
0.083
0.077
D
Andy
1
2
3
4
1.200
1.143
1.100
0.500
4
6
9
1
G
Name
Andy
Andy
Andy
H
Score
89
71
99
@IF(E2>1,@INDEX($A$1..$B$1000,0,F2),"")
is in G2. With the row offset in F2, we can find the name in the A column.
The @IF test eliminates non-matches, because any match must have a value
greater than 1. If the number in the E column is not greater than 1, this
function returns a blank.
@IF(E2>1,@INDEX($A$1..$B$1000,1,F2),"")
is in H2. This returns the score, using rhe row offset in F2. E2..H2 can be
copied down as many rows as desired, as long as numbers in the D column
continue as indicated.
1
2
3
4
5
6
7
8
9
10
11
12
13
A
Name
Dotty
Dotty
Charlie
Andy
Dotty
Andy
Betty
Charlie
Andy
Betty
Betty
Charlie
B
Score
95
73
73
89
95
71
82
88
99
72
73
91
C
0
0
0
1
1
2
2
2
3
3
3
3
D
Andy
1
2
3
4
E
4
6
9
F
Name
Andy
Andy
Andy
G
Score
89
71
99
1
2
3
4
5
6
7
8
9
10
A
Andy
Betty
Charlie
Dotty
Eddie
Fannie
Ginny
Henry
Izzy
Johnny
B
87
96
96
80
78
95
97
98
87
78
C
1
2
3
4
5
6
D
TargetScore
98
97
96
96
95
87
E
Offset
7
6
1
2
5
0
115
F
Place
1
2
3
3
G
Name
Henry
Ginny
Betty
Charlie
H
Score
98
97
96
96
is in G2. This uses the offset in the E column to get the name in A, but if
this is not one of the top three scores, the @IF function leaves it blank.
@IF(F2>0,@INDEX($A$1..$B$1000,1,E2),"")
is in H2. This does the same as the last function in getting the score from
column B.
D2..H2 are then copied into the rows below.
1
2
3
4
5
6
7
8
9
10
A
Andy
Betty
Charlie
Dotty
Eddie
Fannie
Ginny
Henry
Izzy
Johnny
B
87
96
96
80
78
95
97
98
87
78
C
1.000
1.5000
1.333
0.250
0.200
0.167
10.143
100.125
0.111
0.100
D
1
2
3
4
5
6
E
TargetScore
100.125
10.143
1.500
1.333
1.000
0.250
F
Offset
7
6
1
2
0
3
G
Place
1
2
3
3
H
Name
Henry
Ginny
Betty
Charlie
I
Score
98
97
96
96
is in E2. It finds the largest unique value in the C column, as called for by
D2, which contains 1, the value in D2.
@MATCH(E2,$C$1..$C$10,0)
is in F2. It locates in the C column the offset row of the unique value in E2.
@IF(E2>100,1,@IF(E2>10,2,@IF(E2>1,3,"")))
is in G2, which calculates the rank. We cant use @RANK because we created
unique values in column C, so no ties would be possible. This formula does
the same thing by treating anything with a value over 100 as tied for rank 1,
anything with a value over 10 as tied for rank 2, and anything with a value
over 1 as tied for rank 3, but anything equal to or less than 1 as not a top-3
score.
@IF(E2>1,@INDEX($A$1..$B$10,0,F2),"")
is in H2. It takes the offset value in column F and returns the name from A,
as long as the cell in G is not blank.
@IF(E2>1,@INDEX($A$1..$B$10,1,F2),"")
is in I2. It takes the offset value in F and returns the score from B, as long
as the cell in G is not blank.
E2..I2 are then copied into the rows below.
And now, we press on to QPs macros, which will enable the user to
automate much more.
118
Part III
119
Chapter 11
121
Dynamic Modification
Macros may change dynamically. They can consist of text formulas (page
7) that change, depending on the changing content of other cells. Doing so
122
Running Macros
When the macro commands are written in cells in the spreadsheet, the
macro can be run by either:
Pressing [Alt+F2], selecting the starting cell, and clicking [OK].
Associating that cell with a command button (see page 134), and
clicking the button.
Naming the cell in a way that allows a keystroke combination to run
it.
B
{Home}
{D}@TODAY
{U}Today
C
Cursor/cellpointer goes to A1
Goes down one cell; Enters @TODAY function
Goes up; Enters the word Today
Script stops at the blank cell
same cell execute. If it tests false, then the macro skips to the next line
down. Modifying the last illustration, we add {If} tests in B2 and B3 in
Table 11.2.
Table 11.2: Conditional commands with {If}
A
1
2
3
B
{Home}
{D}{If A2=}@TODAY
{U}{If A1=}Today
The net result is that the script will put the @TODAY function only if the
formula A2="" tests true. Otherwise, the macro will do nothing in cell A2.
It will run a similar test in A1, and if A1="" tests as true it will type Today,
but if something is there, the macro will do nothing more.
The Test argument tests a proposition that either evaluates as true
(1, or apparently any non-zero number) or false (0). The Test argument
usually involves equations and inequalities (such as =, >, <, >=, <=, <>),
but it can include functions such as the @IS*** functions. Both sides of an
equation in Test may include formulas or functions. Different equations
can be combined in the Test argument by the connectors #AND# and #OR#.
detour to cell Z1 and start executing commands there, but it would not
return to the original sequence.
The macro in Table 11.3, which fills A1..A4 with 1,2,3,4, illustrates the
difference between subroutines and branches.
Table 11.3: Branching and subroutines
A
1
2
3
4
B
{Let A1,1}
{C2}
{Branch D4}
{Let A4,0}
{Let A2,2}
{Let A3,3}
{Let A4,4}
125
B
{D}{U}{Branch c(0)r(0)}
B
{Let A1,A1+1}
{If A1>10}{Quit}
{Let A2,A1*10}
{Branch B1}
macro tests whether A1 has increased beyond 10, and if so, the macro quits.
Otherwise, the command in B3 places a number 10 times greater than A1
into cell A2. Then the command in B4 reverts to B1. At the end, A1 will
have 11 in it, but A2 will have 100.
It is also common to loop down a column of data and to stop the loop
when the data ends and blank cells begin. See How to test for blanks at 32.
Another technique is to have the macro enter a value at the end of a column
that will tell it when to stop (e.g., 9999 or STOP); loop down the column,
testing the values in it (via @CELLPOINTER or @INDEX or similar commands)
until stop-value is reached, and then optionally delete the stop-value.
Stop# stops the {For} command from executing the macro at MacroCell,
if the value in CounterCell has passed the Stop#. Thus, if the
CounterCell merely equals the Stop#, the macro at MacroCell will
execute.
Step# is the number that the {For} command adds to the CounterCell
after each time it executes the macro at MacroCell. Most of the time,
this number will be 1, but it need not be. It could be 2, if you want
the macro to work on every other item of data. It could be 7 if you
want the macro to work on a particular day of the week. It could be
a negative number, such as -1, if you want to work backwards from
a higher Start# to a lower Stop#, such as searching a database for
matches from the bottom of it rather than the top.
MacroCell is the cell in which a subroutine starts that the {For} command
will play.
The {For} loop in Table 11.6 does the same thing that the last branching
loop did: it runs the command at B3 10 times. The result will be that A1
contains 11, but A2 contains 100. This is because the {For} command will
execute the B3 command even when A1 reaches 10, but when it reaches 11,
it will stop before executing the B3 command.
Table 11.6: A simple {For} loop
A
1
2
3
B
{For A1,1,10,1,B3}
{Let A2,A1*10}
Since the {For} command amounts to calling a subroutine, the subroutine can be halted by a {Return} command. When that happens, the {For}
command behaves as it does when the subroutine has finished normally: it
adds the Step# to the CounterCell and determines whether to run the
subroutine again.
In addition, all repetitions of the {For} command can be halted by the
{Forbreak} command.
Putting these concepts together, I find it useful to structure the subroutine like this:
1. Use an {If} test to see if we should terminate the {For} loop. If so,
use {Forbreak}. This is particularly useful if we have passed the
end of a database and there is no need to process further rows. In
this connection, I typically use an if-test to determine if the macro has
reached a blank cell. For constructing such tests, see the discussion
above at page 32.
128
2. Use an {If} test to see if this instance need not be processed, but
allow for further instances to be processed. If so, use {Return}.
3. Process this instance with one or more appropriate commands.
4. Use an {If} test to see if we should terminate the {For} loop. If so,
use {Forbreak}.
Windows On/Off
One way to prevent the QP window from redrawing is to use the
{WindowsOff} command. At the point that it occurs, the window will
not redraw as the macro executes until the macro ends or a {WindowsOn}
command occurs.
For example, if at the start of a macro, the user is on sheet A,
{WindowsOff} is executed, and then the macro selects a cell on sheet B for
user input with {?}, the user will not see sheet B, only sheet A. The only
indication that the user is on a different sheet will be the Cell indicator
above the Row headers and, perhaps, the Cell Editing box above the Column
headers. If the user should see the context in sheet B, place a {WindowsOn}
command before the {?} command.
Panel On/Off?
Here I note the {PanelOn} and {PanelOff} commands, that are parallel to {WindowsOn} and {WindowsOff}, but which appear obsolete. In the
DOS days, the program had a panel that included a set of menu items on
the top row of the screen and prompts that explained menu items on the next
row. Those would change as the macro executed keystrokes to cycle through
the menu system. {PanelOff} would prevent those menus and prompts
from updating, which in turn would speed up macro execution. {PanelOn}
would return the display to change during macro execution. As far as I can
129
tell, there is no longer any such thing as a panel in QP, and therefore, this
command does nothing.
Redraw Mode
In QPs settings (via Tools > Settings), under the Macro node, there
appear to be four options under the topic Suppress-Redraw: namely, None,
Window, Panel, Both.
It is not clear from this presentation whether None or Both applies to
the verb Suppress or the verb Redraw, and the system is ambiguous:
The help file would suggest that selecting Both would suppress the
redrawing of both the Window and the Panel. (There hasnt been a
Panel since the DOS days, I suspect.)
The language of the macro that automates this choice leaves out suppress, and therefore suggests that selecting Both causes everything
to redraw during macro execution.
My investigation of all permutations suggests that the latter, the macro
command language, is correct. The help file is in error, and the dialog box
for QPs settings is misleading. Thus:
{Application.Macro.Macro_Redraw "Both"}
allows the QP window to redraw during the macro;
{Application.Macro.Macro_Redraw "None"}
suppresses the redrawing.
Usage Comments
How do these two sets of commands work together? My testing of permutations indicates that, unless a different command forces the window to
redraw, either set of options can be used to suppress the redrawing, and
the only way to see the window redraw during the macro using these commands is to set the Macro_Redraw option to Both (either by default or
by using the command {Application.Macro.Macro_Redraw "Both"})
and to use {WindowsOn}.
As we will see (page 150), one way to force the window to redraw, that
overrides any of these combinations, is to use the command {Wait @NOW}.
There is no logical reason why that command should have that undocumented function, or for the help files calling it obsolete.
Recommendation. In view of these findings, my recommendation is that
the programmer leave the Macro_Redraw option set at Both and use only
130
132
Macro
How to save the closing date and time with the file
Give one cell (here, A1) your preferred numeric format for dates, and give
another cell (here, A2) your preferred numeric date for time. Then, in the
macro initiated in the cell named _NBExitMacro, place these commands.
{Let A1,@NOW}{Let A2,@NOW}
And do not forget to include {FileSave}.
133
{SelectBlock +A1}
{PlayPerfectScript},
134
You can also run a PerfectScript macro (or any other program that can
be launched with a command line) using QPs {Exec CommandLine,WindowMode} command. CommandLine is the text string that one would insert
in the Windows Run box, an executable file with any parameters, enclosed
in double quotes. WindowMode is a number that allows the program to
run normal (1); minimized (2), or maximized (3), but by adding 100 to any
of these numbers, QP will wait until the external program/macro finishes
before executing; otherwise, the QP macro and the external program/macro
will work simultaneously. (Caveat: while the QP native macro is running,
the PS macro does not appear to be able to use the changing data in the
spreadsheet.) To run a PS macro, this might be a typical command:
{Exec "C:\Program Files (x86)\Corel\WordPerfect
Office X7\Programs\ps170.exe
/#/m-c:\mymacro.wcm",2}
The CommandLine argument first identifies the PS command interpreter
(this one is for QP17, earlier versions of QP will be slightly different). The /#
switch prevents PS from displaying its interface. The /m- switch identifies
the PS macro to run. The 2 runs the macro minimized.
And if the file is not open, specifying the file name in the first argument will
open the QPW file, run the macro, and then close the file.
qp.ExecMacro("C:\Spreadsheets\MyData.qpw";"C:B1")
136
Chapter 12
would move the cursor to the next column that was not fully visible (column
K). QP may be made to behave that same way by clicking Tools > Settings,
selecting the General node, and checking the Compatibility Keys (QP-Dos)
box. Then {Tab} and {BigRight} will go to the next screen of columns to
the right, as in earlier DOS versions, and {Backtab} or {BigLeft} will go
to the screen of columns to the left. Without checking the DOS compatibility
options, those commands simply behave like the right and left arrow keys.
But without fooling with those subtleties, the keystroke [Ctrl+]
moves right to the next screen of data, as does the macro command
{Ctrl+Right}. [Ctrl+] moves leftward one screen of data, as does
{Ctrl+Left}. Both macro commands can include an optional Repetition
argument.
Likewise, {Ctrl+PgDn} and {Ctrl+PgUp} do what the corresponding
keystrokes do, go to the next sheet in the notebook or the previous sheet,
respectively.
SelectBlock
{SelectBlock CellorBlock<,ActiveCell>} selects the Cell or
Block specified as the first argument. If you select more than one cell
in the CellorBlock argument, the active cell will be the topmost, leftmost
cell in the Block. For example, if the command {SelectBlock E1..H2} is
followed by the command {D 1}, the active cell will be E2.
A unique feature of {SelectBlock} is that it allows you to select which
cell will be active. If you want to select some other cell within the block, add
the optional ActiveCell argument. Thus, if the command {SelectBlock
E1..H2,G2} is followed by the command {D 1}, the active cell will be G3.
ActiveCell should refer to a cell within CellorBlock; if not, QP may
crash.
138
EditGoto
{EditGoto CellorBlock<, ExtendSelection>} selects the Cell or
Block specified as the first argument. If you select more than one cell in the
CellorBlock argument, the active cell will be the topmost, leftmost cell in
the Block.
The unique feature of {EditGoTo} is that it can extend the selection
from the current selection to the CellorBlock specified if the user adds the
optional argument 1, which indicates that the current selection is extended.
If the argument is omitted or 0, {EditGoto} simply goes to the cell or block
and selects it.
To illustrate, the macro in Table 12.1 would first select cells A1..A2 with
{SelectBlock}. The {EditGoto} command would extend that selection
to the entire block of A1..C3.
Table 12.1: Selection by SelectBlock and EditGoTo
A
B
{SelectBlock A1..A2}
{EditGoTo C3,1}
1
2
Earlier methods
Earlier versions of QP used commands that emulate the methods by
which a user manually selects a block. Thus, to select A1..C3, the user would
select A1 and drag the mouse to C3 or hold the [Shift] and use cursor
keys to extend the selection to C3. Using a macro, this would do that:
Table 12.2: Selection by simulating keystrokes
A
1
2
3
B
{SelectBlock A1}
{Shift+Down 2}
{Shift+Right 2}
Other methods
{QGoTo} summons a dialog box for the user to select the destination.
Unfortunately, as designed, it does not stop macro execution, so later macro
139
commands will execute often before the user has the opportunity to deal with
the dialog box, very much contrary to user expectations. It appears that the
{PauseMacro} command was designed to deal with that by stopping further
execution, but unfortunately, at least in the case of {QGoTo}, commands
after {PauseMacro} do not execute. If {QGoTo} is useful at all, it appears
to work as expected only at the end of a series of macro commands.
The {Navigate} command is apparently for selecting the entire table
in which the cursor is located when the command is executed, or for jumping
to one of the sides or corners of the table. The most useful option would be
{Navigate.SelectTable}, which easily identifies the bounds of the table.
@PROPERTY("Active_Block.Selection") will return the coordinates of
the table thus selected. For other uses of {Navigate}, consult the help file.
B
{Let A1,@CELLPOINTER("address")}
{Ctrl+PgDn}
{SelectBlock +A1}
Running the macro in B1 stores the current address (C5) in cell A1,
goes to the next page by doing what happens when you manually press
[Ctrl+PgDn], and then selects the cell C5 on that page.
Table 12.4 shows another way to do it, with a single, more complex macro
command. This command selects the cell that is pieced together by the
@concatenate function. In the example where the cursor starts in A:C5, we
want a function that returns the address B:C5. We use the @CELLPOINTER
141
Table 12.4: Moving to the same cell on the next page with a single command
A
1 {SelectBlock @CONCATENATE(@INDEXTOLETTER(@CELLPOINTER("sheet")),
":",@INDEXTOLETTER(@CELLPOINTER("col")-1),@CELLPOINTER("row"))}
function to get the starting information, which we must then massage. The
@CELLPOINTER functions for the cell A:C5 return these values:
@CELLPOINTER("sheet") . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . returns 1
@CELLPOINTER("col") . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . returns 3
@CELLPOINTER("row") . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . returns 5
The row is correct as is, but the sheet and column have to be converted
into letters, as illustrated in Table 12.5. The @INDEXTOLETTER function
does that, but there is a complication. For @CELLPOINTER, the first sheet,
column and row are always 1, not 0. For @INDEXTOLETTER, the first letter
(A) is always correlated with 0, not 1.
Table 12.5: How different functions treat columns
A
1 1
2 B
B
2
C
C
3
D
D
What @CELLPOINTER(col) returns for A1..C1
What @INDEXTOLETTER returns from numbers in row 1
142
B
{SelectBlock A1}{For A1,1,100,1,B3}{SelectBlock A1}
{; insert one or more rows of macro commands here}
{Ctrl+PgDn}
How to round-trip, to move from one cell to another, and then return
Table 12.7 shows a macro that will take whatever the current cells
content is and paste it to a particular cell elsewhere (e.g., Z1), and then
return.
Table 12.7: Round-tripping
A
1
2
3
4
5
B
{Let A1,@CELLPOINTER(address)}
{EditCopy}
{SelectBlock Z1}
{EditPaste}
{SelectBlock +A1}
143
B
_NBStartMacro
{SelectBlock +A1}
_NBExitMacro
{Let A1,@PROPERTY(Active_Block.Selection)}
{FileSave}
144
Chapter 13
Interface Macros
Here we look at the ways one can give information to the user and get
a response. Note that some ways in the help file, such as {Indicate} are
obsolete and simply do not work.
Sound: {Beep}
QP gives some basic sound options through the {Beep Pitch#} command. According to the help file, the Pitch# argument is optional, but if
so, {Beep} alone makes no sound on my laptop. The Pitch# argument is a
number from 1 to 10, going from low to high.
An external sound can be played using QPs file launching commands,
such as {EXEC}, or through PerfectScript macros. See WPU 29939.
145
all highlighting, fonts, size, alignment, etc. If the block is too narrow
to hold the message, the message is truncated.
ScreenX is the horizontal position for the message to appear, starting
from 0 on the left to the last position on the right of the screen. On
my screen resolution with a horizontal width of 1366, the rightmost
ScreenX value that would display a character was 194. Your monitor
and resolution may differ from this.
ScreenY is the vertical position for the message to appear, starting from 0
on the top to the last position on the bottom. On my screen resolution
with a vertical width of 768, the bottommost ScreenY value that would
display a character was 45. Again, your monitor and resolution may
differ from this.
Expiration This argument is optional. If it is omitted, the message remains until the user presses a key. The message should contain a
number representing the date and time of day (as QP recognizes it)
when the message disappears. For instance, a message that should
expire at 6:00PM on March 18, 2015 would need to include a value of
42081.75. Since we humans do not usually find that method of keeping
time helpful, and since QP would simply hang until that time, the
Expiration argument is usually either left blank or constructed with
@functions like those below.
Thus, to display the message in A1 at screen position 0,0 for 3 seconds:
{Message A1,0,0,@NOW+@TIME(0,0,3)}
This sets the duration of the message by taking the current time (@NOW) and
adds 3 seconds to it (@TIME(0,0,3)) to construct the expiration time that
is three seconds from now. I believe that decimals in the third argument of
the @TIME function are rounded to the nearest integer.
146
B
100
25
Col 100, Row 25
C
{Message B3,B1,B2}
@CONCATENATE("Col ",E4,", Row ",E5)
B
{Alert "Choice","Choose yes or no",A1,4,1,0}
{If A1=6}{alert "Info","You chose YES",A2,0,3,0}
{If A1=7}{alert "Info","You chose NO",A2,0,3,0}
In this simple macro, the {Alert} command in cell B1 first asks the user
to choose Yes or No; ButtonType 4 give the user a yes/no choice, and the
command stores the choice in cell A1. If the user chose Yes (which would put
6 into cell A1), the commands in cell B2 will show another informational
{Alert}, confirming the Yes choice. If the user chose No (which would put
7 into cell A1), the commands in cell B3 would confirm the No choice with
another informational {Alert}.
Quirk. If the text for the title or message is stored in another cell, that
cell must be coded as a formula rather than as pure text. That is, if it
is stored in cell A1, the {Alert} macro command should identify cell A1
as +A1 or =A1 or (A1), but not simply as A1. {Alert} treats a reference
like A1 in the Title or Message arguments like text, even though it is
not wrapped in double-quotes, and thus it would display A1 as the title or
message. {Alert} treats A1 in the ChoiceCell argument as a cell, which
needs nothing further to identify it as a cell.
148
B
{MenuBranch A4..D4}
A2
A3
Exit
{Let A2,1+@SUM(a1..a3)}
{Branch B1}
{Let A3,1+@SUM(a1..a3)}
{Branch B1}
{Quit}
the top row of a DOS screen. B5..E5 would have been explanations for each of
the menu items, but are completely meaningless in a pop-up menu, so those
are left blank. But for backward compatibility, I suppose, the structure still
starts the macro commands on the next row, B6..E6, here. The macros in the
B..D columns place a new number in cells A1..A3, but any macro commands
are available. The {Branch} command at the end of those columns simply
re-starts the process with the pop-up menus. The Exit item in E4 provides a
graceful way out of the otherwise infinite loop.
B
{Let A1,Waiting...}
{Wait @NOW+@TIME(0,0,3)}
{Let A1,The wait is over}
For some reason, it doesnt, as explored in WPU 33199. Hat tip to Jeff
150
B
{Let A1,Waiting...}
{Wait @NOW}
{Wait @NOW+@TIME(0,0,3)}
{Let A1,The wait is over}
B
{Let A1,Working...}{Wait @NOW}
{; whatever commands}
{Let A1,Done!}
The macro in Table 13.6 would place Working... into A1 and immediately display it, then execute any other commands before concluding by
putting Done! into A1. If the processing takes enough time to be apparent,
the user will see Working... in A1.
That, however, would not indicate a progression. Another way to indicate
a progression in, for example, a {For} loop would be to use {Wait @NOW}
with each repetition.
The macro in Table 13.7 would place Working ... in A1, and the {For}
command would put the counter into cell A3 for 1000 repetitions of the
macro at B5. Each time it runs the macro at B5, the {Wait} command
would cause this QP window to redraw, and the changing numbers in A3
would be apparent, thus indicating a progression, until the counter passes
the number stored in cell A4.
151
Table 13.7: Displaying numbers from 1 to 1000 in A3 as macro loops 1000 times
A
1
2
3
4 1000
5
6
B
{Let A1,Working...}
{For A3,1,A4,1,B5}
{Let A1,Done!}
{Wait @NOW}
{; whatever commands}
Caution #1. Since redrawing the window slows performance down, you
may wish to redraw the window only every 10th time, or 50th time, or 100th
time. If so, use the @MOD function in an {If} command like this:
{If @MOD(A3,100)=0}{Wait @NOW}
This line, in lieu of B5 in the above example, would execute the {Wait}
command each time the counter in A3 was evenly divisible by 100. The user
would see the number in A3 progressing from 100 to 200, etc.
Caution #2. The programmer also doesnt want to create redundant
window-redrawing. For instance, if QP is set to redraw the window and
the programmer has not written a {WindowsOff} command (see Speed
and Display Options, at page 129), anything that causes the window to
redraw will cause a slowdown. It appears that, in addition to {Wait @NOW},
commands that would cause QP to change the headers (Row numbers on the
left and column numbers at the top) or the sheet tab cause such redrawing,
e.g., {PgUp}, {PgDn}, {Ctrl+PgUp}, and {Ctrl+PgDn}, as well as navigation to any place off the initial window. In addition, scrollbar operations
using {VLine} and {HLine} would cause a redrawing. In those cases, with
redrawing enabled, adding a {Wait @NOW} command could easily cause
needless slowdowns.
Now, when this macro is run, a bar will appear on the left inside portion
of cell A2, and it will grow larger toward the right as the macro progresses
to its end. Table 13.8 shows what it would look like at the half-way point in
the loop.
Table 13.8: Displaying a progress bar
A
1 Working...
2
3 500
4 1000
5
6
7
B
{Let A1,Working...}
{For A3,1,A4,1,B5}
{Let A1,Done!}
{Let A2,100*(A3/A4)}
{Wait @NOW}
{; whatever commands}
Remember that if this slows the macro excessively, the {Wait} command
can be prefaced by an {If} command with an @MOD function. I often precede
the {For} loop with {WindowsOn} and setting recalculation settings to
manual. See page 129 for details.
B
{Let A1,Working...}
{For A3,1,A4,1,B5}
{Let A1,Done!}
{Let A2,100*(A3/A4)}
{Message A2,0,0,@NOW}
{; whatever commands}
If, instead of cell A2, everything relevant to it were in some other place in
the spreadsheet out of sight, the user would see only the message in the top
left of the screen. However, the message would likely flicker a lot. It displays
only until QP updates its timer, which it does once per second. It does not
display while every other command is executed. If many other commands
are executed, there may be lengthy periods in which it is not displayed. If
153
B
{Let A1,Enter your name}
{SelectBlock A1}
{Edit}{Ctrl+Shift+Home}{?}
If the macro is not to continue with other commands, the {?} pause
command is superfluous, but if the macro will continue with other commands,
pausing until after the user responds in cell A1 will usually be necessary.
Table 13.11 shows a variation on the same macro:
A further development of this idea that I find useful is to place all
messages in a single block of cells that can be edited in that block, including
multi-line messages, but used in macros like the one in Table 13.12. The
154
Table 13.11: Another way to put an input message in the cell itself
A
1 Enter your name
2
3
4
B
{SelectBlock A1}
Enter your name
{Ctrl+Shift+Home}{?}
block of messages is in the D column, and the macro in the B column would
put the multiline instruction from D4 into cell A1.
Table 13.12: Storing input messages for easy revision
A
1
2
3
4
B
{SelectBlock A1}
+D4
{Ctrl+Shift+Home}{?}
D
Enter your name
Enter your address
Enter your phone number
Compose your code this way:
1 First three letters of a color
2 Last three digits of phone number
Table 13.13 extends the concept by adding tests to check whether the
input-type was correct. In this example, the entry of t branches one way,
the entry of n branches another, and if neither was entered, the macro loops
back to request re-entry of a t or an n.
Table 13.13: Adding error checking for in-cell input
A
B
C
1 Type t to add text or n to add a number, and Enter
2
{SelectBlock A1}
3
Type t to add text or n to add a number, and Enter
4
{Ctrl+Shift+Home}{?}
5
{If @LOWER(A1)=t}{Branch B8}
6
{If @LOWER(A1)=n}{Branch C8}
7
{Branch B2}
8
{GetLabel What Text?,A2}
{GetNumber What Number?,A3}
9
The macro beginning in B3 places the question in A1. It asks for the user
to enter t or n in cell A1, and if the user does so, it executes commands at B8
or C8, respectively, but if not, the command in B7 causes the macro to loop
back and put the same question to the user again.
155
156
B
{Get A1}
{If A1="l"}{Shift+Left}{Branch B1}
{If A1="r"}{Shift+Right}{Branch B1}
{If A1="u"}{Shift+Up}{Branch B1}
{If A1="d"}{Shift+Down}{Branch B1}
{If A1="x"}{Blank A1}{Return}
{Beep 4}{Branch B1}
157
158
Chapter 14
B
{GetObjectProperty A:A2,"A:A1.Font.Typeface"}
{SelectBlock A:A1}{GetProperty A:A2,"Font.Typeface"}
Quirk. Not all of the strings return the value expected. Attempting to
determine whether a row or column is hidden fails. See the workaround
using {OnError} in WPU 31127.
159
{SetProperty}, {SetObjectProperty}
These commands allow the macro-writer to change the property of
some part of the QP window. {SetProperty Property,Option} sets
the specified Property of whatever object is selected (cell, block, whatever)
as Option. {SetObjectProperty Object.Property,Option} sets the
specified Property of a specifically named Object as Option. See the
earlier appendix on object properties (page 34).
The following macros at A:B1 and A:B3 in Table 14.2 should both set the
font for cell A:A1 as Courier New.
Table 14.2: SetProperty and SetObjectProperty
A
1
2
3
B
{SetObjectProperty "A:A1.Font.Typeface",Courier New}
{SelectBlock A:A1}{SetProperty "Font.Typeface",Courier New}
Quirk. These commands do not always work as one might expect. For
instance, at least some cell properties can be set after selecting them and
using {SetProperty}, but they cannot be set without selecting the cells
and using {SetObjectProperty}, as noted in WPU 30532.
1
2
3
4
A
{SelectBlock SourceCell}
{EditCopy}
{SelectBlock TargetCell}
{PasteSpecial Properties}
But that method copies all properties. If you want to copy fewer than
all properties, use {SetProperty} or {SetObjectProperty}. Thus, for
example, if you want to copy the same alignment from cell A1 to A2, use
{SetObjectProperty "A2.alignment","A1.alignment"}.
160
1
2
3
4
5
6
7
8
9
10
A
83
66
55
90
62
72
74
96
58
92
B
{If @CELLPOINTER("contents")=""}{Up}{End}{Up}{Quit}
{If @CELLPOINTER("contents")<80}{SetProperty "Text_Color","4"}
{If @CELLPOINTER("contents")<65}{SetProperty "Fill/Pattern";"33;0;Solid"}
{Down}{Branch C1}
161
03/03/15
03/04/15
03/05/15
03/06/15
03/07/15
03/08/15
03/09/15
B
{If @CELLPOINTER(contents)=}{Up}{End}{Up}{Quit}
{If @WKDAY(@CELLPOINTER(contents))<3}
{SetProperty Fill/Pattern;33;0;Solid}
{Down}{Branch c(0)r(-2)}
B
b
b
C
1
2
3 a
4 a
5
6
7
8
9
b
b
b
b
b
2
3
1
2
3
a
a
a
a
a
E
{If @CELLPOINTER("contents")=""}{Up}{End}{Up}{Quit}
{If @CELLPOINTER("contents")=1}
{SetObjectProperty "c(-2)r(0)..c(-1)r(0).Text_Color","4"}
{If @CELLPOINTER("contents")=2}
{SetObjectProperty "c(-2)r(0)..c(-1)r(0).Text_Color","5"}
{If @CELLPOINTER("contents")=3}
{SetObjectProperty "c(-2)r(0)..c(-1)r(0).Text_Color","24"}
{Down}{Branch E1}
The same is easy to set up for shading the cells as shown in Table 14.7.
Table 14.7: Shading cells based on conditions in other cells
A
1 a
2 a
B
b
b
C
1
2
3 a
4 a
5
6
7
8
9
b
b
b
b
b
2
3
1
2
3
a
a
a
a
a
D
{If @CELLPOINTER("contents")=""}{Up}{End}{Up}{Quit}
{If @CELLPOINTER("contents")=1}
{SetObjectProperty "c(-2)r(0)..c(-1)r(0).Fill/Pattern","33;0;Solid"}
{If @CELLPOINTER("contents")=2}
{SetObjectProperty "c(-2)r(0)..c(-1)r(0).Fill/Pattern","29;0;Solid"}
{If @CELLPOINTER("contents")=3}
{SetObjectProperty "c(-2)r(0)..c(-1)r(0).Fill/Pattern","24;0;Solid"}
{Down}{Branch D1}
Setting the cursor in C1 and running the macro in D1 produces the results
shown here.
For similar applications, see Ron Hirschs macro that tests whether a
date in a date column is within one of three ranges, and if so, it applies blue
or red color to 13 cells on the same row (WPU 35861).
Corporate
Insurance Appeal
Contract
Domestic Appeal
Fraud
Contract Appeal
B
Appeal
{If @CELLPOINTER("contents")=""}
{Up}{End}{Up}{Quit}
{If @ISERR(@FIND(C1,@CELLPOINTER("contents"),0))}
{Down}{Branch C3}
{SetProperty "Fill/Pattern.Fill_Color";"9;0;Solid"}
{Down}{Branch B3}
{If @ISERR(@FIND(@UPPER(C1),
@UPPER(@CELLPOINTER("contents"),0)))}
164
Chapter 15
167
168
Data can be specified directly in the command (e.g., 12), it can come indirectly from another cell, whether containing a value or a formula or a
function (e.g., A2), and it can come from a formula or function written
in the command (e.g., +7+5 or @NOW). If Data comes from a blank cell,
{Put} places a zero in the target cell.
By default, any formula or function in Data is converted into a number
or text when it is placed into the appropriate cell of the Block. If the
command places text, the text is preceded by the apostrophe prefix.
The command does not leave a formula or function as a formula or
function.
Data:string places any Data as text into the designated cell withing
Block. Any formula or function, including formulas and functions that
return only text, will appear as text with an apostrophe prefix.
{Put} does not change the formatting of cells in the Block.
Since {Put} does not place formulas, and since its arguments imply
transferring data into a table-like structure, {Put} is an excellent way to
transfer values from one block of data into another, using {For} loops. See
the chapter on retrieving information from databases on page 187.
169
171
Some examples:
To paste formats (properties) only, use:
{PasteSpecial Properties}
To paste a series of formulas as their values, use:
{PasteSpecial Formula cells,Formula Values}
To paste all data with their values, use:
{PasteSpecial Label Cells,Number cells,Formula cells,
Formula Values}
To add formats but without converting formulas to values (and thus to
emulate {EditPaste}), use:
{PasteSpecial Label Cells,Number cells,Formula cells,
Properties}
To do the same but convert formulas to values, use:
{PasteSpecial Label Cells,Number cells,Formula cells,
Formula Values,Properties}
{SetObjectProperty ("Report:"&@ADDRESS(A1,A2)&
".Value")("Data:"&@ADDRESS(A1,A2)&".Value")}
Techniques for coding such loops are given in the chapter on retrieving data
from a database (page 187).
174
it as text in the TargetCell. This is an excellent method of converting numeric formats, and it is unfortunate that there is no @Function that does the
same thing. In particular, there is no good single @Function for converting a
numeric date into a text. {Contents} is a decent second option.
If SourceCell is too narrow, QP displays asterisks through the cell
(e.g., *****), and the optional Width argument circumvents the potential
problem by (the help file says) making QP pretend that the column housing
SourceCell is of a larger width (though it isnt). Placing 1000 in that
argument is pretty safe. However, at least in QP17, this does not actually
appear to be a real problem: {Contents} does not send asterisk to the
TargetCell even if they are displaying in the SourceCell.
The optional NumericFormat# says that a format number from 1 to 127
will impose a particular format on a number. In many cases it does, but
the help file is inaccurate. I used a little macro to generate a list what is
returned when each format number is used for a value of 42071.567. Table
15.2 shows the results.
Table 15.2: Numeric options for {Contents}
Code
0-15
16-31
32-47
48-63
64-79
80-95
96-111
112
113
117
125-126
127
114
115
116
121
122
119
120
123
124
not mentioned
+/- (bar chart)
General
Text
not mentioned
Default
Date [1] (DD-MMM-YYYY)
Date [2] (DD-MMM)
Date [3] (MMM-YYYY)
Date [4] (Long International)
Date [5] (Short International)
Time [1] (HH:MM:SS AM/PM)
Time [2] (HH:MM AM/PM)
Time [3] (Long International)
Time [4] (Short International)
08-Mar-15
08-Mar
Mar-15
03/08/15
03/08
01:36:29 PM
01:36 PM
1:36:29 PM
1:36
The help files examples might have been valid when QP was a DOS
175
1
2
3
4
5
6
7
8
A
Smith, John
Smith, Jane
123 Main St
Anywhere, GA
(478) 555-1212
C
{Search.Reset}
{Search.Block A:A1..A100}
{Search.Direction Row}
{Search.Find @concatenate(B1)}
{Search.Look_In Value}
{Search.ReplaceBy @concatenate(B2)}
{; Search.Match Whole}
{Search.ReplaceAll}
177
[Fill
The if test determines where the current cell has any content. If not, the
type argument evaluates as b and the macro stops. Otherwise, it performs
the variable functions that the programmer fills in. The {D} command
moves the cursor down to the next cell, which may not be necessary if the
commands the user fills in would do that anyway (the or {CR} commands
may be set to do that). The {Branch} command at the end branches back
to the same if-test.
Of course, the macro need not be compressed into a single cell. More
complex editing cannot be compressed into a single cell.
If there are gaps in the column, macros like these may stop before you
want it to stop. In such cases, a {For} loop might be better, or a loop that
begins by asking you how many times to repeat it. In Table 15.5, the user
would supply the block to be edited in cell A1 and run the macro in B1. The
{For} loop in B1 would cycle through the rows of the block, and call the
{For} command in B3, which would in turn cycle through the cells in the
179
block on that row, column-by-column, and run the macro commands starting
in B5, which place the cursor on each cell in the row. Commands in B6 and
below would perform some operation on that cell.
Table 15.5: Basic {For} loop to modify every cell in a block
A
1 D1..F100
2 Row
3 Col
4
5
6
B
{For A2,0,@ROWS(@@(A1))-1,1,B3}
{For A3,0,@COLS(@@(A1))-1,1,B5}
{SelectBlock @OFFSET(@@(A1),A2,A3)}
[Fill in command(s)]
180
181
B
{Blank A1}
{BlockCopy @cellpointer("address"),A1,1,1,0}
{if @cell("type",A1)="b"}{Let A1,"No formula"}{quit}
{let A1,"This contains a formula"}
182
Chapter 16
183
A different macro could be written if you want to display this date and
time as string/text rather than a number. If it is a number, the column in
which it is put needs to be wide enough to display it in full. If it is a string,
it can overlap the next column.
185
186
Chapter 17
Databases: Retrieving
Information
QP has many ways to retrieve information in databases. Well look at
some here, but pass on others like CrossTabs and Forms under Tools > Data
Tools. In the following examples, well work with the following assumptions.
Sheet A will be the users interface with the data.
Sheet B will store reports generated from the data.
Sheet C will contain the macros.
Sheet D will contain the data itself.
I recommend that you keep such functions on separate sheets.
Also, to keep addresses straight, the illustrations of spreadsheets will
show the page letter in the actually blank top left header cell.
The data on Sheet D will be stored in a well-structured database. In
these examples, well assume that the database has unique index values
(invoice numbers) in column A, dates in B, customers names in C, quantities
in D, and prices in E. It will look like Table 17.1.
Table 17.1: Sample well-structured database on Data sheet
D:
A
1 Invoice
2
100
3
101
4
...
B
Date
03/04/15
03/05/15
...
C
Customer
Smith, John
Jones, Mary
...
187
D
Quantity
2.39
2.20
...
E
Price
$295.05
$271.59
...
Well use D:A1..E1000 as the block of data, but you can easily modify
that in the macros below.
In fact, instead of hard-coding this block into each macro command, you
can place that block as text in some cell, and refer to it in macro commands
by using the @@ function. Thus, for instance, if you store D:A1..E1000 in cell
C:A100, then @@(C:A100) in each macro command would refer to it, and
if you later need to expand the block, you simply change the contents of
C:A100.
Many would name the block something like Data for the same purpose,
but given QPs problems with block names noted in the introduction at page
iv, I either hard-code the block or use the @@ function.
Simple @VLOOKUP
With the sort of structure in this sample, it is easy to set up the interface
sheet (A) so that when the user enters a number in a cell (A2), QP displays
relevant information. Hence, in Table 17.2, typing 101 into cell A2 causes
the functions in B2..E2 to display relevant information:
Table 17.2: Retrieving by index with @VLookup
A:
A
1 Invoice
2
101
B
Date
3/5/15
C
Customer
Jones, Mary
D
Quantity
2.25
E
Price
$234.56
B2= @VLOOKUP(A2,D:A1..E1000,1,0)
C2= @VLOOKUP(A2,D:A1..E1000,2,0)
D2= @VLOOKUP(A2,D:A1..E1000,3,0)
E2= @VLOOKUP(A2,D:A1..E1000,4,0)
B
{GetNumber Enter invoice number,A:A2}
{Let A:B2,@VLOOKUP(A:A2,D:A1..E1000,1,0)}
{Let A:C2,@VLOOKUP(A:A2,D:A1..E1000,2,0)}
{Let A:D2,@VLOOKUP(A:A2,D:A1..E1000,3,0)}
{Let A:E2,@VLOOKUP(A:A2,D:A1..E1000,4,0)}
188
B
{For C:A1,0,@ROWS(D:A1..E1000)-1,1,C:B3}
{Put B:A1..E1000,0,C:A1,@INDEX(D:A1..E1000,0,C:A1)}
{Put B:A1..E1000,1,C:A1,@INDEX(D:A1..E1000,1,C:A1)}
{Put B:A1..E1000,2,C:A1,@INDEX(D:A1..E1000,2,C:A1)}
{Put B:A1..E1000,3,C:A1,@INDEX(D:A1..E1000,3,C:A1)}
{Put B:A1..E1000,4,C:A1,@INDEX(D:A1..E1000,4,C:A1)}
Note that B3..B7 could be replaced by a single command the transfers the entire block:
{BlockValues @OFFSET(D:A1,C:A1,0,1,5),@OFFSET(B:A1,C:A1,0)}
In the {For} command in B1, note that the initial counter is 0, because
the rows recognized by both {Put} and @INDEX start with an offset of 0.
That counter is stored in cell A1. Note that the final counter is the number
of rows in the database less 1. That is because the numbering of rows starts
with 1, not 0.
If, however, you prefer to have the row counter in A1 match the rows
on the screen, for aesthetic or other reasons, it is easy to modify the macro
accordingly, as in Table 17.5.
In writing the macro, the programmer must be alert at all times to the
distinction between numbering systems that start with 0 and those that
start with 1.
Finally, you could replace all of the commands with those that use only
the screen coordinates beginning at 1. To do so here,
Replace @INDEX with the combination of @@ and @ADDRESS, which
uses the row and column headers that start with 1. Thus the @INDEX
function in B3 can be replaced by @@("D"&@ADDRESS(C:A1,C:A2)),
which returns the same content.
189
B
{For C:A1,1,@ROWS(D:A1..E1000),1,C:B3}
{Put B:A1..E1000,0,C:A1-1,@INDEX(D:A1..E1000,0,C:A1-1)}
{Put B:A1..E1000,1,C:A1-1,@INDEX(D:A1..E1000,1,C:A1-1)}
{Put B:A1..E1000,2,C:A1-1,@INDEX(D:A1..E1000,2,C:A1-1)}
{Put B:A1..E1000,3,C:A1-1,@INDEX(D:A1..E1000,3,C:A1-1)}
{Put B:A1..E1000,4,C:A1-1,@INDEX(D:A1..E1000,4,C:A1-1)}
Replace {Put} with any of several cell content commands (page 165)
that use screen coordinates, such as {Let}.
If you use this approach, B3 would contain this command:
{Let ("B:"&@ADDRESS(A1,1)),
@@("D:"&@ADDRESS(A1,1))}
In my testing, using the 0 offset with {Put} and @INDEX is a hair faster
than methods that use the screen coordinates. And once you get the knack
of it, easier to program. But there is something to be said for consistently
using only the screen grids coordinate system. And although the choice
between these two macros may be purely aesthetic, one could be preferable
based on whether all or most of the other commands use an offset of 0 or use
column and row numbers with an offset of 1.
Note that there is a gap between B1 and B3. If the commands in B3
started in B2, they would execute again after the {For} loop ended, which
is probably not intended.
Transfer of all data by row and cell, using a double {For} loop.
A variation on this same macro reduces the number of lines of code by
creating a double {For} loop. As shown in Table 17.6, the first in B1 cycles
through the rows, as in the last example, but it then calls the second {For}
loop at C2, which cycles by column through each cell on the row and stores
the column in cell A2. It then calls a single {Put} command which takes
the row and column coordinates from A1 and A2 to place data on the B sheet
in parallel with the D sheet.
And as before, if you prefer to keep row and column counters in A1..A2
equal to the rows and columns of the affected cells, the changes in Table 17.7
would occur.
190
Table 17.6: Transferring data by row and cell with double for loop
C:
A
1 D-Row
2 D-Col
3
B
C
{For C:A1,0,@ROWS(D:A1..E1000)-1,1,C:C2}
{For C:A2,0,@COLS(D:A1..E1000)-1,1,C:B3}
{Put B:A1..E1000,C:A2,C:A1,@INDEX(D:A1..E1000,C:A2,C:A1)}
B
C
{For C:A1,1,@ROWS(D:A1..E1000),1,C:C2}
{For C:A2,1,@COLS(D:A1..E1000),1,C:B3}
{Put B:A1..E1000,C:A2-1,C:A1-1,@INDEX(D:A1..E1000,C:A2-1,C:A1-1)}
B
C
{For C:A1,1,@COUNT(D:A1..E1000),1,C:C2}
{For C:A2,1,@COLS(D:A1..E1000),1,C:B3}
{Put B:A1..E1000,C:A2-1,C:A1-1,@INDEX(D:A1..E1000,C:A2-1,C:A1-1)}
the loop, or simply to ignore that line with a {Return} command. Thus, the
command inserted into C2 in Table 17.9 would stop the {For} loop, as long
as no zeros are in the index column on sheet D.
Table 17.9: Setting the limit by an {If} test and {Forbreak}
C:
A
1 D-Row
2 D-Col
3
4
B
C
{For C:A1,1,@ROWS(D:A1..E1000),1,C:C2}
{If @INDEX(D:A1..E1000,0,C:A1-1)=""}{Forbreak}
{For C:A2,1,@COLS(D:A1..E1000),1,C:B4}
{Put B:A1..E1000,C:A2-1,C:A1-1,@INDEX(D:A1..E1000,C:A2-1,C:A1-1)}
192
B
C
{For C:A1,1,@ROWS(D:A1..E1000),1,C:C2}
{If @INDEX(D:A1..E1000,0,C:A1-1)<200}{Return}
{For C:A2,1,@COLS(D:A1..E1000),1,C:B4}
{Put B:A1..E1000,C:A2-1,C:A1-1,@INDEX(D:A1..E1000,C:A2-1,C:A1-1)}
If you impose multiple tests for each row, it would make logical sense to
start with the one most likely to exclude the row.
To set a default rule of exclusion unless the {If} test includes the row,
follow the {If} test with a {Branch} command that points to commands at
some other location, and no commands are under the {If} test. If the row
satisfies the test for inclusion, the macro executes command at the other
location, and in Table 17.11, the row is transferred to the report sheet. If
the row does not satisfy that test, there are no other commands to execute,
and the {For} command goes to the next row.
Table 17.11: Including rows with invoice number greater than 199, with {If} and
{Branch}
C:
A
1 D-Row
2 D-Col
3
4
B C
D
{For C:A1,1,@ROWS(D:A1..E1000),1,C:C2}
{If @INDEX(D:A1..E1000,0,C:A1)>199}{Branch D3}
{For C:A2,1,@COLS(D:A1..E1000),1,C:B4}
{Put B:A1..E1000,C:A2-1,C:A1-1,@INDEX(D:A1..E1000,C:A2-1,C:A1-1)}
variables into another cell, say A3, and write the macro to compare this row
of data with that cell.
B
C
{For C:A1,1,@ROWS(D:A1..E1000),1,C:C2}
{If @INDEX(D:A1..E1000,0,C:A1)<200}{Return}
{Let C:A3,@COUNT(B:A1..A1000)}
{For C:A2,1,@COLS(D:A1..E1000),1,C:B5}
{Put B:A1..E1000,C:A2-1,C:A3,@INDEX(D:A1..E1000,C:A2-1,C:A1-1)}
For macros that include rows, it would look something like Table 17.13.
Table 17.13: Including rows, but setting the correct row on the report
C:
A
1 D-Row
2 D-Col
3 B-Row
4
5
B C
D
{For C:A1,1,@ROWS(D:A1..E1000),1,C:C2}
{If @INDEX(D:A1..E1000,0,C:A1)>199}{Branch D3}
{Let C:A3,@COUNT(B:A1..A1000)}
{For C:A2,1,@COLS(D:A1..E1000),1,C:B5}
{Put B:A1..E1000,C:A2-1,C:A3,@INDEX(D:A1..E1000,C:A2-1,C:A1-1)}
Summary
The net result is that the macro needs to be structured in this sequence:
1. Preset the Report sheet to include appropriate headers;
194
2. Create a {For} loop to cycle through the rows on the Data sheet;
3. Devise {If} tests to terminate the loop with {Forbreak} (if not already done in setting the parameters of the {For} command);
4. Devise {If} tests to exclude rows on the Data sheet with {Return},
or to include them with a {Branch} command;
5. Determine the next blank row on the Report sheet;
6. (Optional:) Create a {For} loop to cycle through the cells on the
current row of the Data sheet by column;
7. Transfer data from the row on the Data sheet to the blank row on the
Report sheet by one of the commands for placing content in cells (see
the chapter starting at page 165) that allows copying data without navigating to the source: {Let}, {PutBlock}, {Put}, {BlockValues},
{BlockCopy}, or {SetObjectProperty}.
B
C
{For C:A1,2,@COUNT(D:A1..A1000),1,C:C2}
{If @@("D:"&(@ADDRESS(C:A1,4)))<2.25}{Return}
{Let C:A3,@COUNT(B:A1..A1000)+1}
{For C:A2,1,@COLS(D:A1..E1000),1,B5}
{Let ("B:"&@ADDRESS(C:A3,C:A2)),@@("D:"&@ADDRESS(C:A1,C:A2))}
In B1, the {For} command stores the current row of data in C:A1. That
row counter begins at row 2 (which excludes the header row, since that was
already placed on the Report page). The {For} loop cycles through the rows
of D:A1..E1000 until it reaches the last row with data (instead of cycling
until row 1000). The last row is determined by the @COUNT function, though
we could have used an {If} test and the {Forbreak} command instead.
For each row, this {For} command calls the macro at C2.
In C2, the @ADDRESS function gets the address of the cell containing the
quantity, column D (or 4) on the current row, and the @@ function returns
195
the value in that cell. Using the default rule of inclusion, if that quantity
is less than 2.25, the {Return} executes and this row of data is skipped.
Otherwise, the macro proceeds to C3.
In C3, the macro stores in cell C:A3 the next blank row on the Report
page using the @COUNT command again. If 20 rows of data have already
been filled, the next blank row is 20+1. Since @COUNT would return 20, we
add +1.
In C4, the macro starts a second {For} loop, which cycles through the
cells on the current row of data by column number, which column number is
stored in C:A2. For each cell, the macro calls the command at B5.
In B5, the macro uses {Let} to locate the destination cell on the report
page by column (A2) and row (A3), and to place in that cell the data returned
by the @@ function.
6. {PutBlock2 @@("D:"&@ADDRESS(A1,A2)),
("B:"&@ADDRESS(A1,A2))}
7. {BlockCopy ("D:"&@ADDRESS(A1,A2)),
("B:"&@ADDRESS(A1,A2))}
The commands that transferred an entire row, in the order of speed, were:
1. {BlockValues ("D:"&@ADDRESS(A1,1)&".."
&@ADDRESS(A1,5)),("B:"&@ADDRESS(A1,1))}
2. {BlockCopy ("D:"&@ADDRESS(A1,1)&".."
&@ADDRESS(A1,5)),("B:"&@ADDRESS(A1,1)),1,0,1,0,
0,0,0,0}
3. {BlockCopy ("D:"&@ADDRESS(A1,1)&".."
&@ADDRESS(A1,5)),("B:"&@ADDRESS(A1,1))}
In each of these examples, if a row is set in A3 for placing data on sheet B,
A3 should replace A1 in reference to placing data on sheet B.
197
Starting
point
0,0
1,1
0,0
1,1
0,0
1,1
0,0
Current data
cell
Contents of
current data
cell
For loop in cells
Row in Report
Target in
Report
Current Report
Cell
1,1
0,0
1,1
0,0
1,1
0,0
1,1
0,0
1,1
0,0
1,1
a match, the row on the data sheet is equal to the first row in the column
we search plus the offset number that @MATCH returns. That allows us
to transfer the row to the report sheet by now-familiar means. After the
transfer, we have to reset the starting row for the next search to the next
row in the column of data. Then we branch back to perform the search again,
and this continues until @MATCH returns an ERR. Table 17.16 showswhat it
looks like.
Table 17.16: Finding all rows with a Quantity of 4.32 in column D, using @MATCH
C:
1
2
3
4
5
6
7
A
D-Start
D-Row
D-Col
B-Row
B
C
{Let C:A1,1}
{If @ISERR(@MATCH(4.32,@@("D:"&@ADDRESS(C:A1,4)&"..D1000"),0))}{Return}
{Let C:A2,C:A1+@MATCH(4.32,@@("D:"&@ADDRESS(C:A1,4)& "..D1000"),0)}
{Let C:A4,@COUNT(B:A1..A1000)+1}
{For C:A3,1,@COLS(D:A1..E1000),1,C:C6}
{Let C:A1,C:A2+1} {Let ("B:"&@ADDRESS(C:A4,C:A3)),@@("D:"&@ADDRESS(C:A2,C:A3))}
{Branch C:B2}
In B1, the macro sets the beginning data row at 1, which is stored in
A1. The starting row number will be updated each time a match is found.
The starting number is critical in formulating the @MATCH function, which
here looks for the number 4.32 in the D column on the data sheet:
@MATCH(4.32,@@("D:"&@ADDRESS(C:A1,4)&"..D1000"),0)
The "D:"&@ADDRESS(C:A1,4)&"..D1000" component evaluates as D:D1..
D1000, and the @@ wrapper converts that text into coordinates to be searched
by @MATCH. (As in other cases, 4.32 would more likely be placed in a different
cell, say A5 here, and that cell would be written into the function.)
In B2, the loop begins. The first command is an {If} test, which tests if
there is a match in the range. If not, the @MATCH function returns ERR, and
the {Return} (or {Quit}) command suitably stops the loop. If a match is
found, the macro proceeds to B3
In B3, the macro determines the row number of data on which the match
occurs by adding the offset number that @MATCH returns to the starting
number in A1, and it stores that row number in A2.
In B4, the macro determines the next blank row on the report page and
stores it in A4.
In B5, the macro runs a {For} loop like those in the examples of the last
section, which loops through each cell by column in the row of data and calls
the macro in C6, which simply writes it to the next blank row of the report
sheet.
In B6, the macro resets the starting range for the next @MATCH search as
the row below the one on which a match was found.
In B7, the macro loops back to B2, and the process repeats until @MATCH
returns an ERR.
199
The @MATCH technique works only if you can search for an exact match
in the relevant column of data, so more complex testing must be done either
on a row-by-row basis or by creating a helper column of data that can be
searched by an exact-match method.
The above technique constructed the data block to search (C3) using
the @ADDRESS function. @OFFSET can be used as well. After setting the
initial search block (here, D:D1..D1000) and setting the initial offset to
0, the search block can be recalculated after each search by recalculating the offset. If the offset is stored in C:A5 and the number of rows in
C:A6 (here, 1000), this formula will always return the searchable block:
@OFFSET(D:D1,C:A5,0,C:A6-C:A5,1). Using @MATCH on that range will
either return ERR, in which case the loop terminates, or a number, which we
can store in C:A7. The new offset to be put in C:A5 will be C:A5+C:A7+1.
The searchable block formula will give the correct new searchable block, and
loop can continue until an ERR occurs.
Notebook Queries
QP has various ways of making reports by other means, chief of which
is the Notebook Query, available under Tools > Data Tools > Notebook Query
. . . . This technique requires three tables: (1) the Database itself; (2) a
Criteria Table, which contains formulas that QP uses to select records from
the Database; and (3) an Output Table, which contains the headers of the
Database table that should be displayed in the report. For a Database like
the example used here in Table 17.17, I would typically have a separate
page (Here E:) with my criteria table on the top rows and the Output Table
a few rows below it. It might look like this:
Table 17.17: Sample Criteria Table in Green, Output Table in Orange
E:
A
1 Invoice
2
3
4
5 Invoice
6
B
Date
C
Customer
D
Quantity
4.32
E
Price
Date
Customer
Quantity
Price
In the dialog box, one would manually select the Database Cells (here
D:A1..E1000), the Criteria Table (here E:A1..E2), and the Output Cells (here,
E:A5..E5) and press the [Extract] button. Entering 4.32 into cell E:D2,
the query would place every record with 4.32 in the Quantity column into
row 6 and below. A macro can automate that process, as shown in Table
17.18.
200
1
2
3
4
5
A
{Query.Reset}
{Query.Database_Block "D:A1..E1000"}
{Query.Criteria_Table "E:A1..E2"}
{Query.Output_Block "E:A5..E5"}
{Query.Extract}
201
Mandatory terms
To get all rows with Name1 in column X:
@NOT(@ISERR(@FIND(Name1,X2,0)))
To get all rows with Name1 in column X and Name2 in column Y:
@NOT(@ISERR(@FIND(Name1,X2,0)))
#AND#@NOT(@ISERR(@FIND(Name2,Y2,0)))
To get all rows with either Name1 in column X or Name2 in column Y:
@NOT(@ISERR(@FIND(Name1,X2,0)))
#OR#@NOT(@ISERR(@FIND(Name2,Y2,0)))
Excluded terms
To get all rows except those with Name1 in column X:
@ISERR(@FIND(Name1,X2,0))
To get all rows except those with either Name1 in column X or Name2 in
column Y:
@ISERR(@FIND(Name1,X2,0))
#AND#@ISERR(@FIND(Name2,Y2,0))
To get all rows except those with both Name1 in column X and (if) Name2
in column Y:
@ISERR(@FIND(Name1,X2,0))
#OR#@ISERR(@FIND(Name2,Y2,0))
202
Mixture
To get all rows with Name1 in column X but (if) not Name2 in column
Y:
@NOT(@ISERR(@FIND(Name1,X2,0)))
#AND#@ISERR(@FIND(Name2,Y2,0))
To get all rows except those with Name1 in column X unless Name2 is in
column Y:
@ISERR(@FIND(Name1,X2,0))
#OR#@NOT(@ISERR(@FIND(Name2,Y2,0)))
9
10
11
12
13
14
A
D:A1..E10000
E:A1..E2
E:A5..E5
DRow
CCol
ORow
OCol
B
{; initializing, including blanking A4..A7, setting blocks in A1..A3}
{Let A4,A4+1}
{If @INDEX(@@(A1),0,A4)=""}{Quit}
{If A4>=@ROWS(@@(A1))}{Quit}
{Let A5,0}
{If @CELL("type",@@(@OFFSET(@@(A2),1,A5)))="b"}{Branch B9}
{If @CELL("type",@@(@OFFSET(@@(A2),1,A5)))="l"}
{IF @ISERR(@FIND(@INDEX(@@(A2),A5,1),
@HLOOKUP(@INDEX(@@(A2),A5,0),@@(A1),A4,1),0))}{Branch B2}
{If @CELL("type",@@(@OFFSET(@@(A2),1,A5)))="v"}
{IF @INDEX(@@(A2),A5,1)<>
@HLOOKUP(@INDEX(@@(A2),A5,0),@@(A1),A4,1)}{Branch B2}
{If A5<@COLS(@@(A2)-1)}{Let A5,A5+1}{Branch B6}
{Let A6,@COUNT(@@(@OFFSET(@@(A3),0,0,10000,1)))}
{Let A7,0}
{If @HLOOKUP(@INDEX(@@(A3),A7,0),@@(A1),A4,0)<>""}
{Let @OFFSET(@@(A3),A6,A7),
@HLOOKUP(@INDEX(@@(A3),A7,0),@@(A1),A4,0)}
{If A7<@COLS(@@(A3))-1}{Let A7,A7+1}{Branch B12}
{Branch B2}
203
The macro writer will need to initialize the macro by putting the database
into A1, the criteria range into A2, the output range into A3, the three blocks
required by QPs database query, and by blanking the cells A4..A7. These
and other steps are implicit in B1.
B2 sets the row of the database to be tested, so when a row is fully tested,
the macro loops back here. B3 and B4 test conditions for stopping the macro.
B5 resets the testing by starting with the leftmost cell of the criteria
table; the macro will test those cells from left to right, and loop back here
for the next row of data. B6 through B8 test whether there is a test in the
criteria table. If not (B6), the macro proceeds to the next cell (B9). But if
the cell contains text (B7) or a number (B8), the macro looks to see if the
criterion text is contained in text in the relevant database cell or the number
is equal to the number in the relevant database cell. If not, the macro ends
testing of this row and goes to the next row of data (B2). If so, the macro
repeats this loop for each column of the criteria table (B9), and if it reaches
the last cell of the criteria table without failing a test, it proceeds to output.
Note that the test for text in B7 looks for text on a case-sensitive basis.
If one wanted to search on a case-insensitive basis, this formula should be
used after the if-test:
{IF @ISERR(@FIND(@UPPER(@INDEX(@@(A2),A5,1)),
@UPPER(@HLOOKUP(@INDEX(@@(A2),A5,0),
@@(A1),A4,1)),0))}
And if one sought only an exact match, this simpler formula should be used:
{IF @INDEX(@@(A2),A5,1)<>@HLOOKUP(@INDEX(@@(A2),
A5,0),@@(A1),A4,1)}
B10 sets the correct row for the output data, and B11 resets the column to
begin writing to the output table. B12 actually writes the data. The core
idea is to get the field name from the output table, and look for the value at
the intersection of that field name in the database and the current database
row. B12 begins with an if test to determine whether the value at that
intersection is blank or not; if not, it prints it to the output, but if it is blank,
the macro bypasses it. The reason for doing so is that the best function for
obtaining that information would print a blank as a zero. B13 repeats the
B12 process for each cell in the output table, and when the end is reached, it
loops back to the beginning (B14) to test the next row of data.
E:A1..A2
3
4
E:A5..E5
@COUNT(@@(@OFFSET(@@(B1),
0,0,@ROWS(@@(B1)),1)))
Criteria counter
5
6
7
@CELL("type",
@@(@OFFSET(@@(B2),1,B5)))
@INDEX(@@(B2),B5,0)
@MATCH(B8,@@(B9),0)
@OFFSET(@@(B1),0,0,1,
@COLS(@@(B1)))
@MATCH(B8,@@(B9),0)
@OFFSET(@@(B1),0,B10,B4,1)
@CONCATENATE(@IF(B15<>"",
"#AND#",""),"(",B11,"=",
@CHAR(34),B7,@CHAR(34),")")
@CONCATENATE(@IF(B15<>"",
"#AND#",""),"(@ISERR(@FIND",
(@CHAR(34),B7,@CHAR(34),",",
B11,",0))=0)")
10
11
12
13
D
{Notebook.Recalc_Settings "Manual;
Natural;1;Yes;No"}{WindowsOff}
{Blank C:H1..H10000}{Blank C:B15}
{Let C:B5,0}
{recalc C:B6..B13}
{if C:B6="b"}{Branch C:D7}
{if C:B6="l"}
{Let C:B15,C:B15&C:B13}
{if C:B6="v"}
{Let C:B15,C:B15&C:B12}
{If C:B5<@COLS(@@(C:B2)-1)}
{Let C:B5,C:B5+1}{Branch C:B3}
{Putblock C:B15,C:H1}
{BlockCopy C:H1,+C:B16}
{Recalc C:B16}
{Let C:B17,0}
{Recalc C:B18..B19}
{if @iserr(C:B19)}{Branch C:D24}
{Let C:B20,@COUNT
(@@(@OFFSET(@@(C:B3),0,0,10000,1)))}
14
{Let C:B21,0}{Recalc C:B22}
15 Block formula for the helper col- {if @HLOOKUP(C:B22,@@(C:B1),C:B17+C:B19,0)
<>""}{Let @OFFSET(@@(C:B3),C:B20,C:B21),
umn
@HLOOKUP(C:B22,@@(C:B1),C:B17+C:B19,0)}
16 @OFFSET(H1,0,0,B4,1)
{If C:B21<@COLS(@@(C:B3))-1}{Let C:B21,C:B21
+1}{Recalc C:B22}{Branch C:D15}
17 Cumulative offset
{Let C;B17,C:B17+C:B19+1}
18 @OFFSET(H1,B17,0,B4-B17,1)
{Branch C:B11}
19 @MATCH(1,@@(B18),0)
20 Output Row
{Notebook.Recalc_Settings "Background;
Natural;1;Yes;No"}
21 Output Column
{Blank +C:B16}
22 @INDEX(@@(B3),B21,0)
The normal three blocks are set in B1..B3. (Columns A and C, which are
used for documentation, are omitted here for space reasons.) B4 calculates
the number of rows in the database. The macro initializes in D1..D2, which
205
206
Chapter 18
Databases: Adding or
Modifying Data
The basic methods of adding data to a database or modifying data in it
were discussed in connection with commands relating to cell content (see
Chapter starting at page 165) and methods of building reports starting at
page 189.
B
Date
04/05/15
C
Customer
Doe, John
D
Quantity
1.78
E
Price
$219.74
As before, the strategy in the simple macro in Table 18.2 is to find the
row on the first blank row on the data sheet, and to add the data from A6..E6
there.
To vary this slightly, lets say that we dont have the invoice number in
A6, but simply want to add the data in A:B6..E6 with the next available
207
Table 18.2: Simple macro to add a row of data to the bottom of a database
C:
A
1 D-Row
2 A-Col
3
B
C
{Let C:A1,@COUNT(D:A1..A1000)+1}
{For C:A2,1,@COLS(A:A6..E6),1,C:C3}
{Let ("D:"&@ADDRESS(C:A1,C:A2)),@@("A:"&@ADDRESS(6,C:A2))}
B
{Let A:A6,@MAX(D:A1..A1000)+1}
{GetLabel "Enter Date of Transaction (MM/DD/YY) or x to cancel",C:A3}
{If C:A3="x"}{Quit}
{Let A:B6,@DATEVALUE(C:A3)}
{GetLabel "Enter Customer (LastName, FirstName)",A:C6}
{GetNumber "Enter Quantity",A:D6}
{Let A:E6,A:D6*123.45}
{Let C:A1,@COUNT(D:A1..A1000)+1}
{For C:A2,1,@COLS(A:A6..E6),1,C:B12}
{Branch C:B1}
{Let ("D:"&@ADDRESS(C:A1,C:A2)),@@("A:"&@ADDRESS(6,C:A2))}
most totals are created by a simple @SUM function that doesnt change its
range if a row is inserted where the function is. The result would be that
newly inserted data is not within the range, which means that the @SUM
function does not reflect all the data it is intended to reflect.
One option is to have a blank row between the data and the totals, and
the new row is always inserted where the blank row is. That would have the
effect of inserting a row within the range of the @SUM function, which would
adjust appropriately. The simplicity of this approach may compensate for its
lack of aesthetic appeal.
My preference, however, would be to replace the @SUM functions with
those identified earlier that always return coordinates relative to the current
cell (see discussion at 98). Thus, if there are say 300 rows of data, instead of
placing @(SUM D:E1..E300) into cell D:E301, place this function there:
@SUM(@@(@OFFSET(@@("A1..A1"),0,4,@ROW-1,1)))
As long as columns are not inserted to the left of this row (in which case the
number 4 in this formula could be changed), this formula will always sum
the numbers above it in that column.
The line where the insertion should occur is the line containing the @SUM
formula. Finding it depends on whether there is an entry in the A column
on that row (e.g., Totals:). If so, @COUNT(D:A1..A1000) returns that
row number; but if it is blank, we need @COUNT(D:A1..A1000)+1. Lets
assume here that Totals: is in that cell. Table 18.4 shows how to insert the
row of data.
Table 18.4: Inserting a row of data between last row of data and totals beneath it
C:
A
1 D-Row
2 A-Col
3
4
5
B
{Let C:A1,@COUNT(D:A1..A1000)}
{BlockInsert.Rows ("D:"&@ADDRESS(C:A1,1));Entire}
{For C:A2,1,@COLS(A:A6..E6),1,C:B5}
{Let ("D:"&@ADDRESS(C:A1,C:A2)),@@("A:"&@ADDRESS(6,C:A2))}
The only significant addition here is in B2, which inserts a blank row
precisely where we say in B1 that it should be.
Caution. Whenever adding or deleting columns or rows, one must consider
whether it has some effect on data outside the database but on the same
sheet of the notebook. See the discussion of changing database structure at
page 217.
209
210
B
{Selectblock @OFFSET(A:A1,@COUNT(A:A1..A10000),0)}
{If @CELLPOINTER("col")=1}{let []c(0)r(0),[]c(0)r(-1)}{Edit}{Ctrl+Shift+Home}
{If @CELLPOINTER("col")=2}VERSES{Edit}{Ctrl+Shift+Home}
{If @CELLPOINTER("col")=3}{let []c(0)r(0),[]c(0)r(-1)}{Edit}{Ctrl+Shift+Home}
{If @CELLPOINTER("col")>3}{Branch B:B1}
{?}
{If @COUNTBLANK(@@(@ADDRESS(@CELL("row",[]c(0)r(0)),1)&"..
"&@CELL("address",[]c(0)r(0))))>0}{B:D1}
{If @CELLPOINTER("col")=1}{If @ISERR(@MATCH([]c(0)r(0),$C:$A$1..$A$1000,0))}
{Branch B:D3}
{If @CELLPOINTER("col")=2}{If @ISERR(@FIND(".",[]c(0)r(0),0))}{Branch B:D7}
{If @CELLPOINTER("col")=3}{If @ISERR(@VALUE([]c(0)r(0)))}{Branch B:D10}
{If @CELLPOINTER("col")=3}{If @VALUE([]c(0)r(0))<@VALUE([]c(0)r(-1))}
{Branch B:D13}
{Right}{Branch B:B2}
Cell B1 starts the macro by finding the first blank line in the A column
to begin. This is where the loop begins for each new row; the loop for testing
211
212
D3..D5 asks whether the book should be entered into the listing of Bible
books. If so, D4 adds it, and then branches to B12 so the cursor moves to the
next cell for resumed data entry. If not, D5 re-selects the entry so that the
user can make appropriate corrections, and it then re-checks the entry for
errors.
D7..D8 re-highlight the entry so that the user can put the verses in the
B column into the correct format, and it then branches back to B7 for error
checking.
D10..D11 does the same for page references in the C column.
D13..D15 asks whether the user wants to correct the page reference. If
so, the entry is re-highlighted, and when the user makes a new entry in the
cell, the macro branches back to B7 for error checking. If not, the macro
simply moves to the next cell.
Table 18.8: Macro to find particular row in database and put potentially different
data in it
C:
A
1 D-Row
2 A-Col
3
4
B
{Let C:A1,@MATCH(A:A6,D:A1..A1000,0)+1}
{For C:A2,2,@COLS(A:A6..E6),1,C:B4}
{Let ("D:"&@ADDRESS(C:A1,C:A2)),@@("A:"&@ADDRESS(6,C:A2))}
In B1, @MATCH returns the offset from D:A1 at which we find the value
equal to A:A6, but because that uses an offset of 0 for the first row, to convert
it to the row on the screen, we add +1. B2 runs the by-now familiar {For}
loop through the cells in A6..E6, here beginning with column 2 (B), and
calling the command at B4 that overwrites the existing data in the database.
If you want to update only one item of information in one row in a wellformed database, it is often better to do so with a single command. Thus, for
instance, if you want to change the name in column C on an invoice, and the
invoice number is stored at A:A6, and the new name is stored at A:C6, then
a single command is easy:
{Put D:A1..E1000,2,
@MATCH(A:A6,D:A1..A1000,0),A:C6}
This puts the data at A:C6 into the database in column 2 (meaning column
C, since {Put} uses offsets of 0), at the row where the invoice number in
A:A6 can be found.
If you simply want to navigate to the cell in the database in order to
modify the name manually, use a {Selectblock} command with some
addressing macro. Here, it would be:
{SelectBlock @OFFSET(D:A1,
@MATCH(A:A6,D:A1..A1000,0),2)}
How to modify a cell in each row if another condition in the row is true
A macro can easily conditionally modify data in each row. Let us assume
that you want to change the invoice price in column E to give a 5% discount
if the quantity in column D is 3 or more. Here is a straightforward macro.
The macro in B1 runs the macro in B3 for each row of the database. It
uses the starting offset of 0 because the commands in B3 and B4 use those
offsets.
In B3, the macro tests whether the value in column D (column 3, if the
starting offset is 0) is less than 3, and if so, it skips the row by the {Return}
command. If not, it proceeds to B4.
214
B
{For C:A1,0,@COUNT(D:A1..A1000)-1,1,B3}
{If @INDEX(D:A1..E1000,3,C:A1)<3}{Return}
{Put D:A1..E1000,4,C:A1,@INDEX(D:A1..E1000,4,C:A1)*0.95}
In B4, it puts into the E column 0.95 of the value that is already there,
thus making a 5% reduction in the price. The overall macro does this for
every row where the amount in the D column is 3 or more.
215
216
Chapter 19
Databases: Structural
change
Changing Database Structure: {BlockInsert},
{BlockDelete}
You can alter the structure of a database by inserting or deleting rows
and columns. The commands are:
To insert row(s):
{BlockInsert.Rows CellOrBlock,EntireOrPartial}
To insert column(s):
{BlockInsert.Columns CellOrBlock,EntireOrPartial}
To delete row(s):
{BlockDelete.Rows CellOrBlock,EntireOrPartial}
To delete column(s):
{BlockDelete.Columns CellOrBlock,EntireOrPartial}
CellOrBlock. This argument defines the place at which the insertion or
deletion will occur.
If the block is more than one cell high, the commands to insert or delete
rows will insert or delete the same number of rows.
If the block is more than one cell wide, the commands to insert or
delete columns will insert or delete the same number of columns.
EntireOrPartial. This argument requires the programmer to choose
whether to insert or delete an entire row or column (or multiple rows
or columns), or only a partial row or column.
217
Examples
To insert an entire row where row 10 currently is:
{BlockInsert.Rows A10,Entire}
To insert five rows where row 10 is:
{BlockInsert.Rows A10..A14,Entire}
To insert a row where the cursor currently is:
{BlockInsert.Rows c(0)r(0),Entire}
To insert five rows at the current place:
{BlockInsert.Rows c(0)r(0)..c(0)r(4),Entire}
To insert a row in D:A10..E10 only:
{BlockInsert.Rows D:A10..E10,Partial}
cell (which had been the cell below the one containing ERR) is blank. It will
continue in this loop as long as it continues encountering cells containing
ERR. When it doesnt, the macro proceeds to M3.
M3 moves the cursor down, and then loops back to M1. This has the effect
of testing every cell in the column until a blank is reached, and deleting all
rows that have ERR in the column.
219
220
Chapter 20
to a macro, the FileFormat in each is ASCII Text. The macro produces only
tab-delimited text. To have it save as comma-delimited text, use the CSV
FileFormat. The main options are shown in Table 20.1.
Table 20.1: Sample export commands
Desired file format
Command
Comma-delimited CSV {FileSaveAs "C:\Temp\MyData.csv",CSV}
Tab-delimited
{FileSaveAs "C:\Temp\MyData.txt",ASCII Text}
QPs earlier format
{FileSaveAs "C:\Temp\MyData.wb3",QPW v7/v8}
Basic Lotus 123 format
{FileSaveAs "C:\Temp\MyData.wk1","Lotus 1-2-3 v2.x"}
Note that saving the file in CSV format separates each cell on a row by
commas, but places quotation marks only around the text cells, which is
but one of the various ways in which that can be done. Saving the file as
tab-delimited text separates the cells by tabs (ASCII character 9), of course,
but does not wrap text cells in quotation marks.
before importation into WP. Thus, if the notebook contains links in its cells
A1..Z50, something like Table 20.2 should work.
Table 20.2: Creating an intermediate correctly formatted file
1
2
3
4
A
{FileOpen C:\MyFiles\Links.qpw,Update References}
{BlockValues []A:A1..E1000,[]A:A1..E1000}
{FileSaveAs C:\MyFiles\ExportToWP.qpw}
{FileClose}
It can also be saved in some format other than QPW (page 221), and
it perhaps should be saved in a different format if it is to be merged into
WordPerfect (see the next chapter at page 233).
A third technique is to use the {FileExtract} command. {FileExtract ValuesOrFormulas,Block,FileName} extracts a Block to a file
called FileName. The ValuesOrFormulas argument requires the programmer to specify whether formulas in the Block will remain as formulas or
whether they will be converted to values. The Block will appear with all
formats of the cells and data that were in the original file. Unfortunately,
there is no option for saving the data in other formats, such as delimited
text.
Quirks. Unfortunately, though the Values argument does nicely in converting numeric formulas to values, it converts formulas that produce text to
zeros. This is unlike {BlockValues}, which converts both numeric and text
formulas correctly. There is no reason why the data could not be extracted
to a new file with Formulas, the new file opened, the block be converted
to values by {BlockValues}, and the file re-saved, but that should not be
necessary. See the discussion at WPU 30463.
Also, {FileExtract} saves the cursors current position in the original
file as the current position in the new file, which seems like an odd choice
when opening a new file that may only be a block of unrelated text.
Samples. Using the prior example of a database that consists of a certain
number of rows filled within the block D:A1..E1000, the block with data in
it can be identified as @OFFSET(A1,0,0,@COUNT(A1..A1000),5). This
command extracts that block of data into a file called C:\Temp\Extract.qpw,
preserving formulas in the original:
{FileExtract Formulas,@OFFSET(A1,0,0,
@COUNT(A1..A1000),5),"C:\Temp\Extract.qpw"}
To work around the inability to extract data in a different file format, the
new file could then be opened with {FileOpen}, converted to some other
format with {FileSaveAs}, and then closed with {FileClose}.
223
225
B
{blank A2}{Getdirectorycontents A2,+A3}
{if A2=""}{Open +A3,W}{Branch B4}
{if A2<>""}{Open +A3,A}{Branch B4}
{For A1,0,@COUNT(Data:A1..A10000)-1,1,B7}
{Close}
{WriteLn @INDEX(Data:A1..C10000,0,A1)," ",
@INDEX(Data:A1..C10000,1,A1)," ",
@INDEX(Data:A1..C10000,2,A1)}
When the loop finishes, the command in B5 closes the text file and the
macro stops.
B
{Open "C:\Temp\MyData.txt",W}
{For C:A1,0,@COUNT(D:A1..A1000)-1,1,C:B4}{Close}
{For C:A2,0,@COLS(D:A1..E1000)-1,1,C:B6}
{Contents C:A3,@OFFSET(D:A1,C:A1,C:A2),1000}
{If C:A2=@COLS(D:A1..E1000)-1}{WriteLn C:A3}
{If C:A2<@COLS(D:A1..E1000)-1}{Write C:A3,""}
macro stops.
B4 runs a {For} loop through each cell on the row of data by column,
storing the column offset in C:A2. It calls the macro at B6 for each cell.
B6 uses the {Contents} command to transfer the content of the cell to
C:A3 as text. Any other command that converts numbers to text would work,
but numbers must be converted to text because {Write} and {WriteLn}
take only text as arguments, not numbers.
B7 and B8 test whether the cell is the last (rightmost) cell in the database.
If it is, B7 writes it to the output file with a carriage return. If it is not, B8
writes it to the output file, followed by the delimiter.
A
D-Row
D-Col
FileSize
FilePtr
Text
#Fields
Data
B
{Open "C:\Temp\MyData.txt",R}
{FileSize C:A3}
{GetPos C:A4}
{If C:A4>=C:A3}{Close}{Quit}
{Let C:A1,@COUNT(D:A1..A1000)}
{ReadLn C:A5}
{Let C:A6,@LENGTH(C:A5)-@LENGTH(@SUBSTITUTE(C:A5,"",""))+1}
{For C:A2,1,C:A6,1,C:B11}
{Branch C:B3}
{Let C:A7,@FIELD(C:A5,C:A2,"")}
{If @ISERR(@VALUE(C:A7))}{Let @OFFSET(D:A1,C:A1,C:A2-1),C:A7}
{Return}
{Let @OFFSET(D:A1,C:A1,C:A2-1),@VALUE(C:A7)}
1 and stores the sum in C:A2, where these macros have stored the column
number.
B8 then runs a {For} loop for each item in the text, as B7 stored in C:A6.
For each item, it calls the macro at B11, and when that cycle is complete, B9
causes the macro to branch back to B3 to handle the next line of text.
B11 takes each delimited item in the text row, as separated by the
@FIELD function, and places it in cell C:A7. The purpose of doing so is to
test that cell for whether it is a number-like cell or not. If it is not, we will
put it into the corresponding cell of the database, but if it is, we need to
convert it to a value, because unless we do so, the number will be imported
purely as text.
B12 performs the test on whether the delimited item is numberlike. If
it is numberlike, using @VALUE on it will return a value, but if not, using
@VALUE on it will return ERR. If the test in B12 shows that the item is not
numberlike, it uses @OFFSET to locate the corresponding cell in the database
and places it there with a {Let} command, and then returns control back
to the {For} loop to test the next item. If the item is numberlike, the macro
proceeds to B13.
B13 places the numeric value of the delimited item into the current cell,
using @VALUE to convert the number-like text into a number.
A
{EditGoto Data:A1..E1000}
{EditCopy}
{SelectBlock Data:A1}
{FileNew}
{EditPaste}
{FileSaveAs "c:\temp\export1.csv",CSV}
{FileClose}
The macro copies the source data in M1..M2. M3 simply selects a random
cell (otherwise, the entire database remains selected). M4..M5 opens a new
file and pasts the database. M6..M7 save the database in CSV format and
closes it.
B
C
{Open "C:\temp\export2.csv",W}
{For C:A2,0,@ROWS(@@(C:A1))-1,1,C:C3}
229
Col
4
5
6
7
Content
8
9
{Close}
{If @CELL("type",@@(@OFFSET(@@(C:A1),C:A2,0)))="b"}
{ForBreak}
{For C:A3,0,@COLS(@@(C:A1))-1,1,C:B5}
{Contents C:A4,@OFFSET(@@(C:A1),C:A2,C:A3),1000}
{If @NOT(@ISERR(@FIND(",",C:A4,0)))=0}{Write C:A4}
{If @NOT(@ISERR(@FIND(",",C:A4,0)))=1}
{Write @CHAR(34),C:A4,@CHAR(34)}
{If C:A3<@COLS(@@(C:A1))-1}{Write ","}
{If C:A3=@COLS(@@(C:A1))-1}{WriteLn ""}
The user enters the database block into A1 and then runs the macro at B1.
B1 simply opens a text file and B3 closes it. The intervening cell, B2, runs
a {For} loop for each row of the database, calling the macro at C3, which
first tests whether weve reached a blank row (in which case the {For} loop
terminates), and if not, it runs the macro at B5..B9 for each cell in the row,
column-by-column.
B5 places the contents of the cell as appears on screen as text into A4.
B6..B7 test whether there is a comma in that text in A4; if not, B6 writes it
as-is to the CSV file; if so, B7 writes it between double quotes to the CSV
file. B8..B9 test whether the current cell is the last one in the row; if not, it
writes a comma to the CSV file; if so, it uses {WriteLn} to write the end of
the line.
230
Content
B
{Open "C:\Temp\MyWebPage.html",W}
{Writeln "<html><head><title>My Title</title></head>"}
{Writeln "<body><H1 align=center>My Title</H1>
<table border=1>"}
{For C:A2,0,@ROWS(@@(C:A1))-1,1,C:B8}
{Writeln "</table></body></html>"}
{Close}
{If @CELL("type",@@(@OFFSET(@@(C:A1),C:A2,0)))="b"}
{ForBreak}
{For C:A3,0,@COLS(@@(C:A1))-1,1,C:B11}
{Contents C:A4,@OFFSET(@@(C:A1),C:A2,C:A3),1000}
{If C:A3=0}{Writeln <tr>}
{Writeln <td>,C:A4,</td>}
{If C:A3=@COLS(@@(C:A1))-1}{Writeln <\tr>}
231
232
Chapter 21
Exporting QP Data to
WordPerfect
This chapter reviews the ways that QP data can be transferred to a WP
document. Numeric data should be calculated and formatted in QP before
the transfer, simply because it is the better product for that purpose. The
final product will be best beautified and printed from WP. Every available
way should be easy and bulletproof, but unfortunately, there are a number
of problems. This chapter will itemize pros and cons of each method.
As far as I can tell, there are essentially four basic methods to transfer
QP data to WP. In figure 21.1, the methods are highlighted in yellow, and the
file types are in green.
1. Pasting (or PasteSpecial) after copying in QP;
2. Importing (under Insert > Spreadsheet);
3. Merging (under Tools > Merge);
4. Placing by a PerfectScript Macro.
Variations on these methods should be evaluated in terms these factors:
Stability does it sometimes crash, and what must be done to avoid
crashing;
Numeric formats what must be done to keep them as QP has them,
and whether other cell formats come with them;
Post-processing what else is needed after import to get it in the
desired shape;
233
QP8 .WB3
import
Lotus .WK1
Lotus .WKS
QPW
WPD
merge
WP .DAT
run PerfectScript macro
Figure 21.1: Pathways for exporting QP data to WordPerfect
234
Note that the alignments are wrong in several places. The table is also
placed oddly, at one tab stop from the left margin is also odd. The spacing
(padding between the cell walls and the data within the cell disappeared.
The shading and lines are retained, whether or not desired.
Creating a table in WP first and then pasting into that table put all of
this data into the single current cell of the table.
Here, the justification and position was correct, but the lines changed.
Upon first placement, the table horizontally divides the cells in the first and
second columns, though tabbing through those cells causes the division to
disappear. Pasting into the cell of a pre-existing table places all the data in
the one cell.
this text:
This pastes the text with tabs ([Left Tab] in Reveal Codes) between
the data. All alignment, shading, and borders are gone. As in the other
cases, pasting into a table cell places all the data in that cell.
236
21.7,
Though again not apparent in the PDF version of this text, data in the
last three columns of the WP file have blue triangles in the bottom right,
indicating the formulas are present.
238
Often, WP retains the font from QP in the first cell in the merged document. See WPU 36211 and WPU 36806.
Workarounds
Save relevant portions of complex files to single QPW sheets, and
convert those into DAT files. The downside is that two additional files
have to be created for something that should require neither.
Save relevant portions of complex files to some other intermediate
format. See WPU 36480, referring to clipboard as preferred (citing
WPU 36400), but suggesting that one save in WB3, Lotus WK1, XLS,
XLSX, Paradox, comma-delimited, or tab-delimited, citing WPU 35226.
Place the relevant portions of complex files in the clipboard, and then
merge from the clipboard. See WPU 36286 and WPU 36400, both citing
WPU 34097.
Devise a macro that places most things directly from QP into WP.
In short, the workarounds for these problems are essentially to use other
methods documented here.
QPs older .WB3 and Lotuss .WK1 and .WKS formats merge every
row (even if the top row is a header rather than data). Formatting of
the WP form file controls everything except numeric formatting and
text color. I recommend these, and if the header row in QP is not to be
merged, it should not be in the data exported to one of these formats.
Text formats like tab-delimited .TXT and comma-delimited .CSV files
are not always interpreted correctly; sometimes they are interpreted
as one single block of data to merge. When that error is avoided, the
merge conflates some or all items on the same row in tab-delimited
files, and it seems to skip the last row of comma-delimited files.
Paradox .DB does not merge the top row (at least if it is a header row),
but it converts numeric formats to other formats, which defeats the
purpose.
Excel .XLS and .XLSX files reformat dates to add times and change
the default font size of the merged data. .XLS files had to be resaved
in Excel in order to be usable.
A typical PerfectScript macro merging a WB3 file identified in cell A:A1
into a form file identified in call A:A2 might look like Table 21.2.
Table 21.2: Merging with a QP .WB3 file
Application (WordPerfect; "WordPerfect"; Default!; "EN")
Application (qp; "QuattroPro"; Default!)
vDataFile=qp.GetCellValue("A:A1")
vFormFile=qp.GetCellValue("A:A2")
MergePageBreak (On!)
MergeRepeat (1)
MergeBlankField (Remove!)
MergeCodesDisplayRun (Show!)
MergeSelect (All!)
MergeRun (FormFile!;vFormFile; DataFile!; vDataFile; ToNewDoc!)
should supply field names, however, the system must convert them
into field names or otherwise deal with them. A PerfectScript macro
importing it looks like Table 21.3.
Table 21.3: Merging from text files
Application (wp; "WordPerfect"; Default!; "EN")
Application (qp; "QuattroPro"; Default!)
vDataFile=qp.GetCellValue("A:A1")
ImportSetFileName (vDataFile)
ImportSetSource (ASCII!)
ImportSetDestination (MergeData!)
ImportSetSizeToFit (No!)
ImportSetFirstRecFieldnames (No!)
ImportSetAsciiFieldDelimiter ("")
ImportSetAsciiRecordDelimiter ("")
ImportSetAsciiStrip ("")
ImportSetAsciiEncap ("")
ImportSetMacroVariableName ("")
ViewDraft (Yes!)
ImportDoImport ()
Data from Lotuss .WK1 and .WKS formats are placed the same way,
except that merge inserts a Times New Roman font code at the start of
the data. That is easy to delete by macro commands.
Data from QPs earlier .WB3 format is placed the same way, except
that the merge inserts font and font-size codes from the .WB3 file at
the start of the data. These are also easy to delete by macro commands.
Font color codes from the source data were carried over into the merge.
Dates in Excel .XLS and .XLSX files were reformatted to add times. In
other respects, they merged like .WB3, except that the XLS file had to
be re-saved in Excel to be usable.
Paradox .DB would get the nod here, because it converts the top row
to field names automatically (at least if that is what they are in the
source), but it converts numeric formats to other formats, which defeats
the purpose.
A typical PerfectScript macro that imports data from a block set in cell
A:A2 in a spreadsheet identified in cell A:A1 (in any of the spreadsheet
formats identified above) might look like Table 21.4.
A downside to doing all of this is the potential proliferation of files to
perform what should be a simple merge. We can start with (1) a large
complex QPW, from which we extract (2) a single-purpose QPW file, which
we convert to (3) a tab-delimited text file (or some other middle format),
242
which we import into (4) a DAT file, which we merge with a form file, to
create (5) a new WP document. Until Corel incorporates a way to convert
QPW text directly into a DAT file, we cannot skip between files (1) or (2)
directly to (4). The only way to save a step in going reliably from a large
.QPW file to a .DAT is to skip step (2), and when we create the single-purpose
spreadsheet, instead of saving it as a QPW file, we save it in another format.
243
The PerfectScript macro would then look like Table 21.6, with your form
file specified in the last line.
Table 21.6: Merging from the clipboard - Selecting QP data
Application (wp; "WordPerfect"; Default!; "EN")
MergePageBreak (State: On!)
MergeRepeat (NumberToRepeat: 1)
MergeBlankField (State: Remove!)
MergeCodesDisplayRun (Display: Show!)
MergeRun (FormFile!; "C:\MyForms\Envelope.frm"; Clipboard!; ; ToNewDoc!)
modify that content or its format in any desired way, and then type it into
the WP file.
In that connection, the programmer should note that PerfectScript and
QP handle numbers differently. GetCellValue gets numbers without numeric formatting. They use different date numbering systems. Thus,
Instead of returning the number $100.00 from QP, it returns 100.
Instead of returning the date 04/06/15 from QP, it returns the number
42100
To convert a QP date number to the number that PerfectScript
would evaluate as referring to the same date, add 693593. Thus,
QP numbers April 6, 2015 as day 42100, but PerfectScript numbers it 735693. To use WPs better date formatting, the command
DateString(qp.GetCellValue("C4")
+693593;Long!;"MMMM dd, yyyy") makes the conversion. See
Kenneth Hobsons elaboration at WPU 36843.
PerfectScript may interpret a cells content as text rather than a number. If you need for it to be treated as a number, use a technique
for making PerfectScript interpret it that way. I use a technique like
prefacing it with a mathematical statement like this:
vNumber = 0 + qp.GetCellValue("A:A1")
Here are some observations on how to use this method as a generic
document assembly tool. I draft a WordPerfect form file with markers for
the macro to find and interpret. The markers are carets () on both sides of
an instruction. The macro looks for one, gets the instruction between it and
the next caret, performs the instruction, and then loops to look for the next
one, until all instructions have been performed.
If the instruction is a cell in the active QP file, the macro deletes the
instruction, gets the content of that cell, and types it into the same
location. Thus an instruction like Data:A1 would be replaced by
whatever is in cell Data:A1 in the current QP file.
If I want to get a formatted numeric value from a cell in the QP file,
I precede the instruction with # to signal the macro to return the
formatted numeric value. An instruction like #Data:A1 would cause
QP to convert the numeric value in Data:A1 into formatted text in
another cell by using one of the data conversion commands (see page
77). It would place the resulting text from the other cell into the text
of the WordPerfect document.
Other special functions can be written between carets, and the macro
would return other data or perform other functions. Thus, the instruction DATE would place todays date at that location. The instruction
245
?Message would display the Message and ask for the users keyboard
input. Any process or procedure that can be coded in PerfectScript
could be invoked. PerfectScript can also cause QP to execute one of its
native macros with the ExecMacro command and use the results.
246
Chapter 22
This can be generalized by placing the name of the file into a cell that is
named something like FileToLaunch, and then substituting FileToLaunch
for A:A1 in this macro. See the discussion at WPU 31079, where Kenneth
Hobson shows how to do this with the native QP command {Exec}.
Another way to accomplish this goal follows.
247
B
c:\temp\
*.*
{GetDirectoryContents A1,(B1&B2)}
{SelectBlock A1}
{If []c(0)r(0)=}{Home}{Quit}
{Comment.EditURL (B1)&c(0)r(0),(B1)&c(0)r(0)}
{Down}{Branch B5}
A1. This command yields only the file name and its extension, not the path
that is in B1.
The command in B4 places the cursor in A1. Then the loop begins in B5,
which tests whether the current cell is blank, and if so, the cursor goes back
to A1 and the macro stops. Otherwise, the macro proceeds to the next step.
The {Comment.EditURL} command in B6 is QPs rather unlikely way
of creating a clickable link. Both arguments are identical, (B1)&c(0)r(0),
which combines the path and the file name to create the full name of the
file. The first of the arguments is the link to the file in the B1 folder, and the
second is what this cell should display. This will have the effect of causing
each file name in column A to include the full path.
Finally, the commands in B7 drop the cursor to the next cell below and
loop back to B5, to modify each cell in column A in the same way. The result
is that each file is a hyperlink that can now be clicked to have the operating
system execute that file.
By the way, if the files (or web sites) in the A column contained the full
path (or URL), B6 could be replaced by {Comment.EditURL (c(0)r(0))}.
B
Row counter/Sheet Letter
{For B1,65,90,1,B4}{Home}
{SelectBlock @OFFSET(A1,B1-65,0)}
{Comment.EditURL ,@CHAR(B1),(@CHAR(B1)&:A1)}
248
The command in B2 runs the {For} loop that fills cells A1..A26. It stores
the row counter in B1, but the counter runs from 65 to 90. The reason for
these odd numbers is that @CHAR translates them into the capital letters A
to Z.,which is the desired result.
The command in B3 places the cursor into the A column, starting at
offset 0 (65-65).
The {Comment.EditURL} command in B4 uses nothing for an external
link (the first argument), uses the Sheet letter for display in the second
argument, and then combines that Sheet letter with the cell A1 in the third
argument, which provides an internal link to cell A1 on the given sheet.
The result of the loop is a list of sheets A through Z in A1..A26, and
clicking any of those cells takes the user to the page displayed there.
249
Index
@@, 33, 41, 46
@AMnths, 68
@Address, 44, 45, 199
@Array, 11, 25, 30, 67
@Avg, 58
@BlockName(s), 47
@Cell, 27, 32
@CellIndex, 27
@CellPointer, 27
@Char, 29, 33, 49, 52, 79
@Choose, 26, 82
@Clean, 49
@Cols, 90
@Column, 90
@Command, 31
@Concatenate, 7, 49
@Count, 91
@CountBlank, 32, 33, 91
@CountIf, 13, 92
@Date, 66
@DateDif, 67
@DateInfo, 66
@DateValue, 79
@Day, 66
@Dollar, 7, 78
@DollarText, 78
@EMnth, 68
@Field, 50
@Find, 51, 163
@Fixed, 7, 77
@Fraction, 7, 79
@HLookUp, 95
@Hour, 69
@If, 25
@Index, 95
@IndexToLetter, 91
@Int, 59
@IsBlank, 29, 32
@IsBlock, 29
@IsErr, 29
@IsEven, 29
@IsLogical, 29
@IsNA, 29
@IsNonText, 29
@IsNumber, 29
@IsOdd, 29
@IsString, 29
@Largest, 58
@LastCellValue, 92
@Left, 50
@Length, 51
@LetterToIndex, 91
@Lookup, 93
@Lower, 51
@Match, 26, 110, 198
@Max, 13, 58
@MaxLookup, 98
@Mid, 50
@Min, 58
@MinLookup, 98
@Minute, 69
@Mod, 26, 60, 151
@Month, 26, 66
@NWkDay, 68, 74
@Now, 65
@Offset, 45, 200
@Proper, 51
@Property, 30
@PureAvg, 58
@PureMax, 58
@PureMin, 58
@Rank, 59
250
@Repeat, 49
@Replace, 50
@Right, 50
@Round, 59
@Row, 90
@Rows, 90
@Second, 69
@SetString, 50
@Sheets, 90
@Smalles, 58
@String, 7, 77
@SubTotal, 57
@Substitute, 50
@Sum, 13, 57
@SumIf, 13, 57
@SumNegative, 57
@SumPositive, 57
@Time, 69
@TimeValue, 79
@Today, 65
@Total, 57
@Trim, 49, 52
@Trunc, 59
@Type, 30
@Upper, 51
@VHLookUp, 95
@VLookup, 93, 188
@Value, 79
@Weekday, 67
@Wkday, 67
@WorkDay, 68
@XIndex, 95
@Year, 66
{?}, 126
{Alert}, 147
{BackTab}, 137
{Beep}, 145
{BigLeft}, 137
{BigRight}, 137
{Blank}, 176
{BlockCopy}, 170
{BlockDelete}, 217
{BlockInsert}, 217
{BlockValues}, 169
{Branch}, 124
{Calc}, 131
{ClearContents}, 176
{ClearFormats}, 176
{Close}, 224
{Comment.EditURL}, 247
{Contents}, 174
{Ctrl+PgDn}, 138
{Ctrl+PgUp}, 138
{Define}, 125
{Down (D)}, 137
{EditClear}, 176
{EditCopy}, 172
{EditGoto}, 139
{EditPaste}, 172
{Exec}, 134, 136, 145
{FileClose}, 222
{FileExtract}, 222
{FileNew}, 222
{FileSaveAs}, 221
{FileSave}, 132
{FileSize}, 224
{For}, 127, 189
{Form}, 210
{GetDirectoryContents}, 225, 247
{GetLabel}, 149
{GetNumber}, 149
{GetObjectProperty}, 159
{GetPos}, 224
{GetProperty}, 159
{Get}, 149, 156
{Goto}, 138
{HLine}, 156
{If}, 123, 137
{Indicate}, 150
{Left (L)}, 137
{Let}, 145, 166
{MenuBranch}, 149
{Message}, 145
{Navigate}, 140
{OnError}, 159
{Open}, 224
{PanelOff}, 129
{PanelOn}, 129
{ParseExpert}, 227
{PasteSpecial}, 160, 172
{PauseMacro}, 140
{PgDn}, 137
251
{PgUp}, 137
{PlayPerfectScript}, 134
{PutBlock2}, 168
{PutBlock}, 168
{PutCell2}, 166
{PutCell}, 166, 183
{Put}, 168
{QGoto}, 139
{Quit}, 124
{ReCalcCol}, 131
{ReCalc}, 131
{ReadLn}, 224
{Read}, 224
{Return}, 124
{Right (R)}, 137
{SaveHTML}, 231
{Search}, 176
{SelectBlock}, 138
{SetObjectProperty}, 157, 160, 173
{SetPos}, 224
{SetProperty}, 160
{Subroutine}, 124
{Tab}, 137
{Up (U)}, 137
{VLine}, 156
{Wait}, 126, 150, 151
{WindowsOff}, 129
{WindowsOn}, 129
{WriteLn}, 224
{Write}, 224
Blanks
How to test, 32
Cells
Address options, 27
Attributes, 27
Height, 33
In-cell formatting, 182
Prefix, 28, 32
Type, 28
Width, 33
Commenting (;)
Formulas and functions, 23
Macros, 122
Conditional formatting, 161
Connectors
AND, OR, NOT, 23
Conventions, iii
Custom numeric formats, 85
Dates
Windows settings, 80, 183
Export to WordPerfect
Copy and Paste, 234
ImportDoImport, 236
Merging by Perfectscript macro,
244
Merging from other formats, 240
Merging from other formats via
.DAT, 241
Merging from QP directly, 239
Merging via the clipboard, 243
Formulas
Basic, 3
Block, 11
Commenting formulas, 23
Text, 7
Functions
Commenting formulas, 23
Conditional, 25
Converting text and numbers, 77
Coordinates, 41
Database, 89
Date and Time, 65
Math, 57
Properties, 27
Text, 49
Helper columns, 111
Import/Export to Text, 221
Macros
~(tilde), 122
\+0, 132
\+Letter, 132
Cell content, 165, 174
Cell Properties, 159
Command buttons, 134, 157
Commenting macros, 122
252
Palette, 37
PerfectScript, 134, 244
Application command, 135
Eval command, 135
ExecMacro command, 135
GetCellValue, 135
Properties
Application, 37
Cell or Block, 34
Notebook, 36
Page, 35
Pseudo-column technique, 54, 74
Reference Style
c(0)r(0), 6
R1C1, 6
253