March 28, 2024, 02:25:36 PM

News:

Own IWBasic 2.x ? -----> Get your free upgrade to 3.x now.........


Aurora Tutorial Part 2.

Started by Ionic Wind Support Team, June 07, 2006, 01:09:27 AM

Previous topic - Next topic

0 Members and 1 Guest are viewing this topic.

Ionic Wind Support Team

June 07, 2006, 01:09:27 AM Last Edit: September 10, 2006, 11:33:49 AM by Paul Turley
Part 2 - Continuing with Aurora

In Part 1 of this tutorial we covered basic program structure, console I/O, loops, conditional statements and writing subroutines.  In Part 2 we are going to cover file operations, pointers, using external libraries, and basic OOP usage.

----------------------------------------------------------
Pointers, references, and type-casting...oh my!
Pointers are arguably the least understood concept new programmers face. They are also the number one source of errors you will encounter while programming. With that said you will find that they are impossible to live without.

A pointer is, simply stated, a variable that holds the memory address of another object. When we say object we are not talking about a class object, although a pointer to a class is a common use, but instead anything in your program that exists in memory. 

Aurora supports both typed pointers and untyped pointers. A typed pointer contains the address of the object and the compiler knows the type of the object it points to.  An untyped pointer is just an address and in order to use it a typecast must be specified.

Once a pointer has been assigned a memory address, or reference, the memory can be accessed using a dereference operator. Aurora use the * character as a dereference operator and the -> symbol combination to access a member of a structure, class, or to call a class or interface method when a pointer is given as a left hand operand.

Let's begin with a typed pointer example.  A typed pointer is defined by using the * character:

int *memory;

The above statement is comprised of three parts.  'int' is the type of data, '*' tells the compiler that it is a pointer, and 'memory' is the name of the pointer variable. 

The next step is to assign an address to the pointer variable.  Aurora uses the & symbol to mean address of.  Which is common in other languages, but not really a necessity since the compiler is smart enough to know what to do when you assign a variable to a pointer.  For readability it should be used anyway.

int iValue;
memory = &iValue;

At this point 'memory' contains the address of the integer variable 'iValue'.  Now that the pointer is initialized we can modify the contents of the variable indirectly through the pointer.  The operation of dereferencing a pointer is commonly called indirection.

*memory = 1;

The statement above embodies both the simplicity and power of pointers.  What is happening is the compiler is taking the memory address that is stored in 'memory' and storing the value of 1 into that address. On the outset you might be thinking that it is a round about way of assigning a value to a variable, and you would be right.  Think about it on a broader scale though, such as a subroutine modifying a variable whose address has been passed as a parameter, and you will begin to see how important the pointer can be.  Passing a variable BYREF is essentially the same as passing a pointer to that variable, only the syntax differs.

This example demonstrates basic typed pointer usage:


global sub main()
{
int *memory;
int iValue;
//assign the pointer the address
//of iValue
memory = &iValue;
//Change iValue through the pointer
*memory = 1;
//print iValue through dereferencing and directly
print("*memory = ",*memory," iValue = ",iValue);
//print the address stored in 'memory'
print("'memory' contains the address: ",hex$(memory));
//send the address of iValue to a subroutine
//it will modify the contents
PointerDemo(&iValue);
print("'iValue' now contains: ",iValue);
print("Press any key to close");
while GetKey() == "";

}

sub PointerDemo(int *i)
{
*i = 99;
}


Typecasting
Typecasting allows access the data a pointer is referencing as a different type.  Or in the case of an untyped pointer it is required to let the compiler know what type of data you are accessing.  In Aurora you specify the typecast surrounded by parenthesis during a dereferencing operation. 

*(type_of_data)pointer_name

As mentioned earlier a pointer can be typed or untype.  A void pointer is one of the ways you can create an untyped pointer in Aurora.  If we were to modify our example above to use an untyped pointer it would look like this:


global sub main()
{
void *memory;
int iValue;
//assign the pointer the address
//of iValue
memory = &iValue;
//Change iValue through the pointer
*(int)memory = 1;
//print iValue through dereferencing and directly
print("*memory = ",*(int)memory," iValue = ",iValue);
//print the address stored in 'memory'
print("'memory' contains the address: ",hex$(memory));
//send the address of iValue to a subroutine
//it will modify the contents
PointerDemo(&iValue);
print("'iValue' now contains: ",iValue);
print("Press any key to close");
while GetKey() == "";

}

sub PointerDemo(void *i)
{
*(int)i = 99;
}


As you can see using an untyped pointer requires telling the compiler what type of data we are referencing.  A void pointer is still a pointer, it just does not have a preset type.  The real use of an untyped pointer is to be able to access memory as any type of data.  Which brings up another topic, the NEW operator. When you allocate memory using the NEW operator you are given a chunk of memory to store whatever data your program needs.

The NEW operator takes a type and a count.

void *data = NEW(byte, 100);

Internally Aurora is multiplying the length of the type times the count.  We could allocate the same amount of memory by doing:

void *data = NEW(INT, 25);

An integer is 4 bytes long * 25 = 100 bytes.  The important thing to remember is that NEW just returns a pointer to an allocated block of memory the requested size.  The memory doesn't have a type and can be used for whatever reason you see fit.  When you are done with the memory you must return it to the system by using the DELETE operator.

DELETE data;

With that information we can easily create dynamic arrays.  Arrays are normally a fixed size as the compiler needs to know the size of the array at compile time.  By using NEW, DELETE and a pointer you can create a single dimensional array of any size.  Using a typed pointer the only difference between accessing a fixed single dimensional array and a dynamic one is the * operator.

Example of a dynamic array:


global sub main()
{
//pointer to word sized data
word *array;
int size;
print("Enter the size of the array > ",);
size = StrToNum(readln());
if(size <= 0)
size = 1;
array = new(word,size);
//fill in the array with some random values
for(int x = 0; x < size; x++)
{
*array[x] = rand(1000);
}
//show the contents of the array
print("\nArray contains:");
for(x = 0; x < size; x++)
{
if(x == size-1)
print(*array[x])
else
print(*array[x],",",);
}
//delete the array
delete array;
print("\n\npress any key to close");
while GetKey() == "";
}


Pointer math
A pointer contains an address and that address is a 32 bit unsigned integer value. Since it is a value we can make the pointer look at a different address by using any math operator.  This is known as pointer math and it is a very powerful tool.  We know that a dynamic array of same sized data can be created and accessed using a typed pointer.  Using typecasting and pointer math you can access the memory as different types of data.

As an example problem let's say that you wanted to store some floating point data and wanted a header that contains the number of data items with a description.  After the description we want a variable number of floating point numbers.  It is common for programs to store binary data in this manner and this is one method of accessing that data.


global sub main()
{
//pointer to any sized data;
void *mydata,*pData;
int size;
string dataname;
print("Enter the number of data items > ",);
size = StrToNum(readln());
print("Enter the name of the data set > ",);
dataname = readln();
if(size < 0)
size = 0;
//our header is one integer and one string.
mydata = new(byte, len(int) + 255 + len(float) * size);
//use a second pointer for accessing the data
pData = mydata;
//store the number of data items;
*(int)pData = size;
//move the pointer to store the string
pData += len(int);
//store the data set name
*(string)pData = dataname;
//move the pointer to store the data
pData += 255;
//store some random numbers as the data items.
for(int x=0;x < size;x++)
{
*(float)pData = rnd(1000.0);
pData += len(float);
}
print("---------------------");
//now we have a block of memory that has a header
//containing the number of data items and a string description
//followed by the data itself.  Print it all out to verify.
//reset the pointer
pData = mydata;
size = *(int)pData;
print("Number of items: ",size);
pData += len(int);
dataname = *(string)pData;
print("Dataset name: ",dataname);
pData += 255;
if(size)
{
print("Data Items:");
for(x = 0; x < size; x++)
{
if(x == size-1)
print(*(float)pData)
else
print(*(float)pData,",",);
pData += len(float);
}
}
//delete the array
delete mydata;
print("\n\npress any key to close");
while GetKey() == "";
}


The example used typecasting for all references.  A simplification could have been done by using a pointer to a float and accessing the floating point data in a typed manner.

When using pointer math it is important to remember that adding or subtracting from a pointer is done without regard to the type of data it references.  That is all math is performed byte wise. 

Member access operator
We briefly mentioned the -> operator in the previous section.  This operator goes by many names depending on what language reference your reading. Arrow operator, member selection operator, pointer to member, element selection operator, and member access operator are a few of the names we have seen.  No matter what you call it the member access operator performs the same function in nearly every language you will encounter it in.

In Part 1 of this tutorial we touched on the subject of structures, or UDT's as they are commonly called.  When you have a variable of a structure you access the members of that structure using the dot operator.  But what if you had a pointer to memory allocated with NEW and wanted to use a typed pointer to access it as a structure?  That is where the -> operator comes into play.

point *pt = NEW(point,1);
pt->x = 10;
pt->y = 20;

The -> operator is performing two tasks for you.  It is dereferencing the pointer, and accessing a member of the structure using the equivelent of the dot operator.  The above code can be written as:

point *pt = NEW(point,1);
*pt.x = 10;
*pt.y = 20;

Which will produce the exact same machine code when it is compiled.  The only difference is in syntax and readability.  The -> operator only works on typed pointers and is the preferred method of member access in this case.  If you are using an untyped pointer you must use a typecast and in as much use the second method of accessing.

void *pt = NEW(point,1);
*(point)pt.x = 10;
*(point)pt.y = 20;

The member access operator is also used to call class and COM interface methods. We will go into more details on that later on. For now let's change our last example to create a memory area that has a header consisting of an integer and a string followed by a variable number of structure records:


struct dailyprice
{
float open;
float high;
float low;
float close;
int volume;
dstring date[11];
}

global sub main()
{
//pointer to any sized data;
void *mydata,*pData;
dailyprice *price;
int size;
string dataname;
print("Enter the number of days > ",);
size = StrToNum(readln());
print("Enter the name of the data > ",);
dataname = readln();
if(size < 0)
size = 0;
//our header is one integer and one string.
mydata = new(byte, len(int) + 255 + len(dailyprice) * size);
//use a second pointer for accessing the data
pData = mydata;
//store the number of data items;
*(int)pData = size;
//move the pointer to store the string
pData += len(int);
//store the data set name
*(string)pData = dataname;
//move the pointer to store the data
pData += 255;
//store some random stock info as the data items.
price = pData;
//start with an arbetrary date
int jd = DateToJD(4,1,2005);
//make sure it starts on a monday through friday
if((jd % 7) > 4 )
jd+= 8 - (jd % 7);
for(int x=0;x < size;x++)
{
price->open = rnd(100,200);
price->high = rnd(200,225);
price->low = rnd(75,100);
price->close = rnd(100,200);
price->volume = rand(1000,10000);
price->date = JDToDate(jd);
price += len(dailyprice);
jd++;
if((jd%7) == 5)
jd+=2; //only trades monday through friday ;)
}
print("---------------------");
//now we have a block of memory that has a header
//containing the number of data items and a string description
//followed by the data itself.  Print it all out to verify.
//reset the pointer
pData = mydata;
size = *(int)pData;
print("Number of items: ",size);
pData += len(int);
dataname = *(string)pData;
print("Dataset name: ",dataname);
pData += 255;
price = pData;
if(size)
{
print("\ndaily data:");
print("date\t\topen\thigh\tlow\tclose\tvolume");
for(x = 0; x < size; x++)
{
print(price->date,"\t",price->open,"\t",price->high,"\t",price->low,"\t",price->close,"\t",price->volume);
price+=len(dailyprice);
}
}
//delete the array
delete mydata;
print("\n\npress any key to close");
while GetKey() == "";
}

sub DateToJD(int month,int day,int year),INT
{
int jd,a,m,y;
a = (14 - month)/12;
m = month + (12*a) - 3;
y = year +4800-a;
jd = day + ((153*m+2)/5) + (365*y) + (y/4) - (y/100) + (y/400) - 32045;
return jd;
}

sub JDToDate(int JD),STRING
{
int a = JD + 32044;
int b = (4*a+3)/146097;
int c = a - (b*146097)/4;
int d = (4*c+3)/1461;
int e = c - (1461*d)/4;
int m = (5*e+2)/153;

int day   = e - (153*m+2)/5 + 1;
int month = m + 3 - 12*(m/10);
int year  = b*100 + d - 4800 + m/10;
return using("0##/0##/####",month,day,year);
}


The example above is a bit longer than others as I wanted to use a few julian date routines to create arbitrary monday - friday date strings. 


File Operations

Aurora handles file operations using simple functions to open, close, read and write to a file.  The OPENFILE function creates or opens a file and returns a handle that will be passed to other file functions to operate on that file.

When using the OPENFILE function you specify the full path to the file and what mode the function should use.  The available modes are:

MODE_READ - The file is opened for reading, if the file does not exist the funciton will fail and return NULL.
MODE_WRITE - The file is opened for writing, if the file does not exist the function will fail and return NULL.
MODE_CREATE - Creates a new file. If the file already exists it will be truncated to 0 length.
MODE_APPEND - Opens a file for writing and moves the file pointer to the end of the file. If the file does not exist it will be created.

The modes can be combined to allow any operation on a file you desire.  For example if you want to open an exisitng file to add data to the end of it, and also want to be able to read the file, you would specify MODE_READ | MODE_WRITE | MODE_APPEND.

This short example creates a file called 'test.file' in the directory the executable is located in.


global sub main()
{
unsigned int hFile;
hFile = OpenFile(GetStartPath()+"test.file",MODE_CREATE | MODE_WRITE);
if(hFile)
{
print("File was created");
CloseFile(hFile);
}
else
print("File could not be created");
print("Press any key to close");
while GetKey() == "";
}


As indicated in the above example any successfully created or opened file must be closed with the CloseFIle function before your program exits. If your program leaves a file open it will remain locked until the next reboot of the system.

Reading and Writing to files

After a file has been opened in the needed mode it can be written to with the WRITE function.  WRITE takes a handle to the file, a variable write, and the number of bytes to write.  For binary files use the length of the variable type and for text files convert the variable to a string using either the USING function or the NUMTOSTR function.

It is important to note that the only difference between a binary and text file is the addition of newline characters at the end of each line.  As far as the system is concerned a file is comprised of a sequence of bytes, nothing more.

As an example we will write the numbers 1 to 10 in binary to the file named 'test.file' and read them back.


global sub main()
{
unsigned int hFile;
hFile = OpenFile(GetStartPath()+"test.file",MODE_CREATE | MODE_WRITE);
if(hFile)
{
print("File was created");
for(int x = 1;x<11;x++)
{
Write(hFile,x,len(int));
}
CloseFile(hFile);
//now open the file again and read what was written
hFile = OpenFile(GetStartPath()+"test.file",MODE_READ);
if(hFile)
{
int num;
print("Data in file = ",);
while(Read(hFile,num,len(int)) == 0)
{
print(num,",",);
}
print();
CloseFile(hFile);
}
}
else
print("File could not be created");
print("Press any key to close");
while GetKey() == "";
}


The READ function takes as its parameters a handle to a file opened for reading, a variable to store the bytes read from the file, and the number of bytes to read.  As indicated above READ returns 0 on success or 1 on failure.

Text files
When working with text files it is necessary to read each line of a file as a separate entity.  If you were to use the READ function you would have to find each return character and separate the lines manually.  Fortunately Aurora provides the READSTRING function which will sequentially read each line of a text file, one at a time, into the string variable that you specify.

The READSTRING function takes a handle to the file, a string variable, and the maximum number of characters to read.  It is important to specify the maximum length to avoid overwriting the string.  The maximum length should be the size of the string - 1 to account for the NULL terminator. 

This example creates a small text file, reads it back, and then displays the contents in the console.


global sub main()
{
unsigned int hFile;
string line;
hFile = OpenFile(GetStartPath()+"test.file",MODE_CREATE | MODE_WRITE);
if(hFile)
{
print("File was created");
for(int x = 1;x<11;x++)
{
line = "This is line #" + NumToStr(x) + "\n";
Write(hFile,line,len(line));
}
CloseFile(hFile);
//now open the file again and read what was written
hFile = OpenFile(GetStartPath()+"test.file",MODE_READ);
if(hFile)
{
print("Contents of file:");
while(ReadString(hFile,line,254))
{
print(line);
}
print();
CloseFile(hFile);
}
}
else
print("File could not be created");
print("Press any key to close");
while GetKey() == "";
}


The above example shows that when writing a string to a file to be used as text that you must add the newline character to the string.


-------------------------------

Please don't reply to this topic until the tutorial is finished.  As usual I will be updating it frequently.
Ionic Wind Support Team

Ionic Wind Support Team

Ionic Wind Support Team

Ionic Wind Support Team

Ionic Wind Support Team

Haim

Thanks for a great tutorial!
In your sample code you included Julian date functions JDtoDate() and DatetoJD(), which I find very useful.
I did not understand how you calculated the day of week of any specific Julian Date.
An explanation would be appreciated.
Haim



Ionic Wind Support Team

Julian dates are based on modulo 7 (makes sense since there are 7 days in the week).  So any date % 7 gives you the day of the week starting with 0 for monday and ending with 6 for sunday.  I didn't write them of course, it's been a well known date format for decades.

Subtracting two julian dates will give you the number of days between of course. 

If your really interested see:

http://en.wikipedia.org/wiki/Julian_day

Ionic Wind Support Team

Haim


Ionic Wind Support Team

Ionic Wind Support Team