Tuesday, January 06, 2009

More Macro Tidbits

If you notice (or care) that the blog looks different, I changed to a template that isn't fixed-width. The other template kept wrapping code snippets, which I find can be confusing. One of these days I'll get around to making my own template, but for now this was the least ugly variable-width one that Blogger provides. If code wraps now, stretch your browser window. Or get a bigger screen.

First, I'd like to do my best to answer a question asked about one of my previous posts. An anonymous person (leave names, people, please!) asked:

What is hard for me for doing macros is the format? How and when to use () spaces and such.

This is basically about syntax, so let's focus there. For the most part, syntax is absolutely critical in pretty much any programming language. Computers are very good at doing exactly what we ask them to do, but not so good at doing what we really wanted them to do. The difference is in the syntax.

Syntax Your Brain

There are a couple of nuances in the I-DEAS macro language to be aware of. For one, although you can run a single program that will bounce back and forth between Modeler and Drafting, they actually have separate languages. The languages are very similar, and understanding one for the most part will help to understand the other, but they are distinct. I've spent most of my time in the Drafting macro language and found it to be far more forgiving than the Modeler one. This may be related to the fact that Drafting has excellent documentation and Modeler doesn't. A quick sample of the difference between the languages:

C: This line is a comment

C : This line is a comment

Note the space before the colon in Modeler. This isn't necessarily a huge deal, although it would be nice if they were the same, but the main problem is that Modeler does not provide useful error messages that help direct you to fix a syntax error. I've had several occasions in Modeler where it turned out that the real problem was I put in an extra space somewhere; it was a big deal in Modeler, but an equivalent situation in Drafting would have been just fine. Without access to software, I'm not able to generate any sample error messages, but trust that I've spent some quality time arguing with Modeler about what should and shouldn't be legal. I usually lose.

The next interesting thing about the macro commands is that they can provide more than a single response when asked a question. If you read through the documentation, each command help page will provide a full description of the command that looks something like this:
Read Paper Size ( [Xsize], [Ysize], [Units], [DimStd], [Text] )

I won't recreate the entire page here, but underneath this command is a description of each parameter. For example, the [Units] parameter is of type Integer, it is an output variable (as opposed to input), and the description reads "Drawing units, where 0 = inch, 1 = metric" so it is basically a boolean. Xsize and Ysize should be obvious enough, DimStd refers to the dimensional standard (ANSI, ISO, etc) being used, and Text will report a standard size (A,B,C,D) if applicable, or "EXPLICIT" if the size is custom. So with a single command, you can retrieve a lot of different information in several different forms.

First, you define your variables, maybe something like this:
# REAL Width_Of_Sheet
# REAL Height_Of_Sheet
# INTEGER Unit_System
# INTEGER Dims_Std
# STRING Sheet_Size

(The necessary variable types were in the help document for the command) Then you plug those variables into the command like so:
# Read Paper Size (Width_Of_Sheet, Height_Of_Sheet, Unit_System, Dims_Std, Sheet_Size)

First of all, note that the [] used in the help document are not actually used in the macro. It's basically placeholder notation in the help files. Running this single command will populate all five of these variables with information. To verify this, you could do the following:
# Show Message ("Width_Of_Sheet is: " + Width_Of_Sheet)

If it is blank, something went wrong, otherwise you should see the width of your sheet. You could add four more message commands, one for each variable. I actually recommend doing so as a troubleshooting tip.

But what if you don't need all of these values? Maybe you are only interested in the height of the page. You can do that, but the kicker is you still have to provide the commas. This is so that I-DEAS will know which variable you actually want to populate. Your new command would look like this:
# Read Paper Size (, Height_Of_Sheet,,,)

I-DEAS now knows that you would like to get information supplied to the second parameter in the command, which happens to be for the height of the sheet. If that looks odd, basically what you are doing is this:
# Read Paper Size (Width_Of_Sheet, Height_Of_Sheet, Unit_System, Dims_Std, Sheet_Size)

So if you are ever handed someone else's code, and see all of those crazy commas, what's happening is that the original author did not wish to take advantage of everything the command had to offer. In case the variable names aren't useful enough on their own, you'll want to visit the help document for the given command to research which parameters are being used or ignored.

In some cases, if possible, it may be worth the effort to work with a simpler command. In this particular case, since what I want is the size of the drawing, I could also use this command:
Inquire Drawing Information ([dname], [sizex], [sizey], [partNumber], [version], [revision], [libStatus], [writepriv])

Here I have eight different parameters to play with, but I really don't care about the version, or the revision, etc. Read Paper Size may be a better, more concise, option. It all depends on what information you will need later. If the revision will be useful, then Read Paper Size will not be enough on it's own, and you are going to wind up querying the same information again when you Inquire Drawing Information. Not every situation has the choice between multiple commands, however, so generally you are looking for the only one that will provide the information you need (and feeling pretty lucky if you find one).

I'm afraid I don't have any other real tangible tips regarding syntax. Refer to the documentation and/or existing programs as much as possible for guidance.

I Object

I'm still relatively new to object oriented programming (OOP), having only dabbled a bit as I've tried to learn Mac/iPhone programming over the last couple of years. I still struggle with some concepts here and there, but for the most I think I "get it": modular, reusable code. The I-DEAS macro language is not object oriented. But, through some careful planning you can still achieve a mostly modular result.

I, of course, was not smart enough to think of this when I first started writing my programs. About the second or third time I write the same basic code to do the same basic thing, it finally set in that I was doing more work than I needed to. Unfortunately, ripping out existing code to reformat around a modular approach can be challenging, and you risk breaking code that already works. But in the long run, you do yourself a huge favor by only having to fix code in one place, and the various modules make it easier to create new programs.

Let's take an easy example, applying the current date. I have several cases where I would want to do this. When a user creates a new drawing, I like to automatically provide the creation date on the face of the print. Same thing for when they add a revision. No problem, let's dive into the help documents to find a command to help out. Ah, this looks good:
Inquire Date ( Doption, [Padding], Date )

Reading through the rest of the document, Doption is basically Date Option (MM-DD-YY or DD-MM-YY, for example), Padding essentially lets you force 2 digits for dates, so 03 for March instead of just 3, and then the Date is the output. The previous code examples did not include any input parameters, but this one does, which I need to do in order to format the results the way I want. The only problem is that the actual format I want - MM/YY - is not one of the built-in options. I do not see any other commands that get the current date, so I'm going to have to find a way to work with this command. The help document notes that the Date parameter is a text string, and I just happen to know that there are commands for slicing and dicing text strings up, so maybe I can extract the information I need and then build my own date string.

First, the actual inquiry command:
# Inquire Date ("dmy", 1, cur_date)

dmy sets the output format to be dd-mm-yy, and the 1 forces each section to be two digits. This is a key setting, because it will be more complicated to slice my date string up if I don't know exactly which characters I need. On 1-1-09, the month is the 3rd character, and the year is 5th and 6th. But on 11-11-09, month becomes the 4th and 5th characters, and the year shifts, too. By forcing each section to have 2 digits, I know which characters to aim for, regardless of the actual date. The result is dumped into the cur_date variable. Now to slice it up.

The help files have a page called Functions, and it describes the various math and string functions that are available. Probably the only one that will help here is STRMID(str1,a,b) which is defined as "Access a subset of "str1" beginning at character number "a" and up to and including character number "b" (if a=1, assumes start with first character)". Take a string, start at one character, go until another, and copy that out to make a new string. Perfect. But there is a pesky dash between MM-YY, so I can't just pull it all out in a single string. Oh well, do it twice, and assign the result to a different variable:
# NewDate = STRMID(cur_date,4,5) + "/" + STRMID(cur_date,7,8)

I take the 4th and 5th characters for MM, add my slash, then the 7th and 8th characters for YY. I now have today's date formatted the way I want it.

So what's the big deal? It's only two lines of code, how hard is that to copy and paste periodically? In this case, it's not a big deal. But if the code were more involved, there are more chances for bugs, and that bug would get repeated every time I copy-n-paste it until I actually fix it, at which point I then need to go revisit every program that contains the code and fix it again. I know I don't have my entire program suite memorized, so I'll probably forget to fix it somewhere, and good luck if anyone else has to figure it out in my place. But it doesn't even have to be about bugs. Let's say that we change our minds about the format of the date, and now I want MM/DD/YY. It sure would be nice to only have a single place to make that change, and then all of the other programs get it for free. And that's exactly what I'll do. Rather than leaving this code buried inside of other programs, I'll pull it out into its own program:
# IF (trace EQ 1) THEN Show Message ("}}}IN{{{ Entering get_date.prg")

C: --- Determine date
# Inquire Date ("dmy", 1, cur_date)
# IF (trace EQ 1) THEN Show Message ("cur_date is: " + cur_date)

C: --- Reformat date to MM/YY
# NewDate = STRMID(cur_date,4,5) + "/" + STRMID(cur_date,7,8)
# IF (trace EQ 1) THEN Show Message ("NewDate is: " + NewDate)

# IF (trace EQ 1) THEN Show Message ("{{{OUT}}} Done finding date...")


Anytime that this is run, I know that the variable NewDate will be populated with a date in my desired format. So in any other program where I would want that date, I just need to run this program like so:
# Execute Macro ("C:\get_date.prg",,)

That's mostly all that needs to happen. There are some additional considerations that I'll discuss at a later time, but for the most part this works really well. I have several of these single-purpose little modules: get user name, get sheet size, get all attributes out of a symbol, check the permissions on a drawing, and so on. This way I don't have to reinvent the wheel each time I start a new program. I utilize whatever pieces I already have, and then focus on what will be custom to my new program. For the most part, once the modules work, I've found them to be very reliable, so if errors crop up, I generally know to focus my attention on other areas of the code. Or, if there is a problem in a module, (well, for one it's probably causing a problem in a lot of different places) I fix it one time in one place and get all of the downstream benefits.

So there you have it. It's certainly not OOP in its purest form, but it is about as close as I could manage within the I-DEAS macro paradigm.

No comments: