### Bookstore

 Computer Math and Games in Pascal (preview) Lazarus Handbook

### Author Topic: Fractions  (Read 40125 times)

#### HappyLarry

• Full Member
• Posts: 155
##### Fractions
« on: March 22, 2015, 12:41:06 am »
Are there any built-in procedures or functions in Pascal/Lazarus for dealing with fractions (for instance 2/3 + 1/5) without turning the answer into a floating point number?
Use Lazarus and Free Pascal and stand on the shoulders of giants . . . very generous giants. Thank you.

#### Bart

• Hero Member
• Posts: 5228
##### Re: Fractions
« Reply #1 on: March 22, 2015, 12:51:32 am »
I gues you want:
Code: [Select]
`  writeln(2/3 + 1/5); `
And the output should be 13/15 (literally)?
You'll have to do this yourself.
Define your own type to hold the fraction, and (if you want) overload the arithmetic operators (+ - / *).

(B.t.w. 2/3 in pascal actually is is float.)

Bart

#### Bart

• Hero Member
• Posts: 5228
##### Re: Fractions
« Reply #2 on: March 22, 2015, 01:11:23 am »
Code: [Select]
`program fractions;{\$mode objfpc}{\$H+}uses  Classes, sysutils;type  TFraction = record    numerator,    denominator: Int64;  end;function FractionToStr(F: TFraction): String;begin  Result := IntToStr(F.numerator) + '/' + IntToStr(F.denominator);end;//from http://wiki.lazarus.freepascal.org/Greatest_common_divisorfunction GreatestCommonDivisor(a, b: Int64): Int64;var  temp: Int64;begin  while b <> 0 do  begin    temp := b;    b := a mod b;    a := temp  end;  result := aend;function AddFractions(F1,F2: TFraction): TFraction;var  GCD: Int64;begin  Result.denominator := F1.denominator * F2.denominator;  Result.numerator := F1.numerator * F2.denominator + F2.numerator * F1.denominator;  GCD := GreatestCommonDivisor(Result.numerator, Result.denominator);  Result.numerator := Result.numerator div GCD;  Result.denominator := Result.denominator div GCDend;function CreateFraction(n, d: Int64): TFraction;begin  if (d = 0) then raise EZeroDivide.Create('denominator cannot be 0');  Result.numerator := n;  Result.denominator := d;end;var  F1, F2, Res: TFraction;begin  F1 := CreateFraction(2,3);  F2 := CreateFraction(1,5);  Res := AddFractions(F1, F2);  writeln(FractionToStr(F1),' + ',FractionToStr(F2),' = ',FractionToStr(Res));end.`
Outputs:
C:\Users\Bart\LazarusProjecten\ConsoleProjecten\fractions>fractions
2/3 + 1/5 = 13/15
Code: [Select]
`Bart`

#### typo

• Hero Member
• Posts: 3051
##### Re: Fractions
« Reply #3 on: March 22, 2015, 02:11:04 am »
I have a runtime error 103 in this line:

Code: [Select]
`writeln(FractionToStr(F1),' + ',FractionToStr(F2),' = ',FractionToStr(Res));`

#### Mike.Cornflake

• Hero Member
• Posts: 1259
##### Re: Fractions
« Reply #4 on: March 22, 2015, 02:41:42 am »
Worked here as a console app.  Laz 1.5 (SVN 48027), FPC 3.1.1
Lazarus Trunk/FPC Trunk on Windows [7, 10]
Have you tried searching this forum or the wiki?:   http://wiki.lazarus.freepascal.org/Alternative_Main_Page
BOOKS! (Free and otherwise): http://wiki.lazarus.freepascal.org/Pascal_and_Lazarus_Books_and_Magazines

#### typo

• Hero Member
• Posts: 3051
##### Re: Fractions
« Reply #5 on: March 22, 2015, 02:53:43 am »
My error, sorry.

#### HappyLarry

• Full Member
• Posts: 155
##### Re: Fractions
« Reply #6 on: March 22, 2015, 12:06:33 pm »
I have adapted the ucomplex unit  for complex numbers
(which is very easy to follow) and added some of  Bart's code to create a Fractions unit which lets you use the operators + - * / = as usual and deals with negative fractions.

Code: [Select]
`Unit Fractions;// based on the ucomplex unit on complex numbers{\$ifndef VER1_0}{\$INLINE ON}{\$define TEST_INLINE}{\$endif VER1_0}interface{\$ifndef FPUNONE}uses Sysutils;type    Fraction = record                     num : integer;                     den : integer;               end;    pfraction = ^fraction;//Assignment if the operand has no denominatoroperator := (z:integer) q : Fraction;    {\$ifdef TEST_INLINE}    inline;    {\$endif TEST_INLINE}{ four operator : +, -, * , /  and comparison = }//Additionoperator + (q1, q2 : Fraction) q : Fraction;    {\$ifdef TEST_INLINE}    inline;    {\$endif TEST_INLINE}operator + (q1 : Fraction; z : integer) q : Fraction;    {\$ifdef TEST_INLINE}    inline;    {\$endif TEST_INLINE}operator + (z : integer; q2 : Fraction) q : Fraction;    {\$ifdef TEST_INLINE}    inline;    {\$endif TEST_INLINE}//Subtractionoperator - (q1, q2 : Fraction) q : Fraction;    {\$ifdef TEST_INLINE}    inline;    {\$endif TEST_INLINE}operator - (q1 : fraction; z : integer) q : Fraction;    {\$ifdef TEST_INLINE}    inline;    {\$endif TEST_INLINE}operator - (z : integer; q2 : Fraction) q : Fraction;    {\$ifdef TEST_INLINE}    inline;    {\$endif TEST_INLINE}//Multiplicationoperator * (q1, q2 : Fraction) q : Fraction;    {\$ifdef TEST_INLINE}    inline;    {\$endif TEST_INLINE}operator * (q1 : Fraction; z : integer) q : Fraction;    {\$ifdef TEST_INLINE}    inline;    {\$endif TEST_INLINE}operator * (z : integer; q2             : Fraction) q : Fraction;    {\$ifdef TEST_INLINE}    inline;    {\$endif TEST_INLINE}//Divisionoperator / (q1, q2 : Fraction) q : Fraction;    {\$ifdef TEST_INLINE}    inline;    {\$endif TEST_INLINE}operator / (q1 : Fraction; z : integer) q : Fraction;    {\$ifdef TEST_INLINE}    inline;    {\$endif TEST_INLINE}operator / (z : integer; q2 : Fraction) q : Fraction;    {\$ifdef TEST_INLINE}    inline;    {\$endif TEST_INLINE}//Equals or comparisonoperator = (q1, q2 : Fraction) b : Boolean;    {\$ifdef TEST_INLINE}    inline;    {\$endif TEST_INLINE}operator = (q1 : Fraction; z : integer) b : Boolean;    {\$ifdef TEST_INLINE}    inline;    {\$endif TEST_INLINE}operator = (z:integer; q2 : Fraction) b : Boolean;    {\$ifdef TEST_INLINE}    inline;    {\$endif TEST_INLINE}operator - (q1 : Fraction) q : Fraction;    {\$ifdef TEST_INLINE}    inline;    {\$endif TEST_INLINE}{ functions to do with fractions }function FractionToStr(q:Fraction) : string;function CreateFraction(n, d: Int64): Fraction;function GreatestCommonDivisor(a, b: integer): Integer;//Simplification to the canonical formFunction Canonicalize(q1:Fraction):Fraction;implementationoperator := (z : integer) q : Fraction;  {\$ifdef TEST_INLINE}  inline;  {\$endif TEST_INLINE}  begin       q.num := z;       q.den := 1;  end;  { four base operations  +, -, * , / }//additionoperator + (q1, q2 : Fraction) q : Fraction;  {\$ifdef TEST_INLINE}  inline;  {\$endif TEST_INLINE}    { addition : z := z1 + z2 }  begin       q.num := q1.num*q2.den + q2.num*q1.den;       q.den := q1.den * q2.den;  end;operator + (q1 : Fraction; z:integer) q : Fraction;  {\$ifdef TEST_INLINE}  inline;  {\$endif TEST_INLINE}  begin       q.num := q1.num + z*q1.den;       q.den := q1.den;  end;operator + (z : integer; q2 : fraction) q : Fraction;  {\$ifdef TEST_INLINE}  inline;  {\$endif TEST_INLINE}  begin       q.num := z*q2.den + q2.num;       q.den := q2.den;  end;//Subtractionoperator - (q1, q2 : Fraction) q : Fraction;  {\$ifdef TEST_INLINE}  inline;  {\$endif TEST_INLINE}    { addition : z := z1 + z2 }  begin       q.num := q1.num*q2.den - q2.num*q1.den;       q.den := q1.den * q2.den;  end;operator - (q1 : Fraction; z:integer) q : Fraction;  {\$ifdef TEST_INLINE}  inline;  {\$endif TEST_INLINE}  begin       q.num := q1.num - z * q1.den;       q.den := q1.den;  end;operator - (z : integer; q2 : Fraction) q : Fraction;  {\$ifdef TEST_INLINE}  inline;  {\$endif TEST_INLINE}  begin       q.num := z*q2.den - q2.num;       q.den := q2.den;  end;//multiplicationoperator * (q1, q2 : Fraction) q : Fraction;  {\$ifdef TEST_INLINE}  inline;  {\$endif TEST_INLINE}  begin       q.num := q1.num * q2.num;       q.den := q1.den * q2.den;   end;operator * (q1 : Fraction; z:integer) q : Fraction;  {\$ifdef TEST_INLINE}  inline;  {\$endif TEST_INLINE}  begin       q.num := q1.num * z;       q.den := q1.den;  end;operator * (z : integer; q2 : Fraction) q : Fraction;  {\$ifdef TEST_INLINE}  inline;  {\$endif TEST_INLINE}  begin       q.num := z * q2.num;       q.den := q2.den;  end;//divisionoperator / (q1, q2 : Fraction) q : Fraction;  {\$ifdef TEST_INLINE}  inline;  {\$endif TEST_INLINE}  begin       q.num := q1.num * q2.den;       q.den := q1.den * q2.num;  end;operator / (q1 : Fraction; z:integer) q : Fraction;  {\$ifdef TEST_INLINE}  inline;  {\$endif TEST_INLINE}  begin       q.num := q1.num;       q.den := q1.den * z;  end;operator / (z : integer; q2 : Fraction) q : Fraction;  {\$ifdef TEST_INLINE}  inline;  {\$endif TEST_INLINE}  begin       q.num := z * q2.den;       q.den := q2.num;  end;// equalsoperator = (q1, q2 : Fraction) b : Boolean;begin       b := (q1.num = q2.num) and (q1.den = q2.den);end;operator = (q1 : Fraction; z : integer) b : Boolean;begin       b := (q1.num = z) and (q1.den = 1);end;operator = (z : integer ; q2 : Fraction) b : Boolean;begin       b := (q2.num = z) and (q2.den = 1);end;//negative fractionsoperator - (q1 : Fraction) q: Fraction;begin       q.num := q1.num*-1;       q.den := q1.den;end;// function to write out a fractional valuefunction FractionToStr(q:Fraction) : string;begin        result := inttostr(q.num)+'/'+inttostr(q.den);end;function CreateFraction(n, d: Int64): Fraction;var     q:Fraction;begin      if (d = 0) then raise EZeroDivide.Create('denominator cannot be 0');      if (d < 0) then      begin           q.num := n*-1;           q.den := d*-1;      end      else      begin           q.num := n;           q.den := d;      end;      Result := q;end;function GreatestCommonDivisor(a, b: integer): integer;var    temp: Int64;begin    while b <> 0 do    begin      temp := b;      b := a mod b;      a := temp    end;    result := a  end;Function Canonicalize(q1:Fraction):Fraction;var    GCD :integer;    q2:Fraction;begin    GCD := GreatestCommonDivisor(q1.num, q1.den);    q2.num := q1.num div GCD;    q2.den := q1.den div GCD;    Result:=q2;end;{\$else}implementation{\$endif FPUNONE}end.`
I have tested it (although it should not be used in programs to send humans to Mars yet).
Code: [Select]
`program project2;uses Fractions;var  q1, q2, q3, q4:Fraction;  q:string;begin  writeln('Fraction test');  q1:=CreateFraction(2,3);  q2:=CreateFraction(1,5);  q:=FractionToStr(q1);  writeln(q);  q:=FractionToStr(q2);  writeln(q);  q3:=q1+q2;  q:=FractionToStr(q3);  writeln(q);  q3:=q1-q2;  q:=FractionToStr(q3);  writeln(q);  q3:=q1*q2;  q:=FractionToStr(q3);  writeln(q);  q3:=q1/q2;  q:=FractionToStr(q3);  writeln(q);  q3:=4;  q:=FractionToStr(q3);  writeln(q);  q3:=CreateFraction(-21,33);  q3:=Canonicalize(q3);  q:=FractionToStr(q3);  writeln(q);  q4:=CreateFraction(-7,11);  If q3 = q4  then  begin     writeln('yes');  end  else  begin     writeln('No');  end;  If q3 = CreateFraction(-7,11)  then  begin     writeln('yes');  end  else  begin     writeln('No');  end;  q4:=CreateFraction(3,-4);  q:=FractionToStr(q4);  writeln(q);  q4:=CreateFraction(-3,-4);  q:=FractionToStr(q4);  writeln(q);  readln;end.`
Here is the output
Code: [Select]
`Fraction test2/31/513/157/152/1510/34/1-7/11yesyes-3/43/4`
All corrections and improvements gratefully received. Many thanks to Bart for his code and suggestions.
« Last Edit: March 22, 2015, 12:09:48 pm by HappyLarry »
Use Lazarus and Free Pascal and stand on the shoulders of giants . . . very generous giants. Thank you.

#### wp

• Hero Member
• Posts: 11748
##### Re: Fractions
« Reply #7 on: March 22, 2015, 12:20:57 pm »
Are there plans to add fraction support to math? There are number formats in spreadsheet files for fractions which I skipped so far in fpspreadsheet but would like to bring in. The main routine needed here is conversion of floats to fractions.

#### Bart

• Hero Member
• Posts: 5228
##### Re: Fractions
« Reply #8 on: March 22, 2015, 12:40:51 pm »

Code: [Select]
`program fractions;{\$mode objfpc}{\$H+}{\$MODESWITCH  ADVANCEDRECORDS}uses  Classes, sysutils;type  { TFraction }  TFraction = record    Numerator,    Denominator: Int64;    procedure Init(ANumerator,ADenominator: Int64);    procedure Normalize;    function ToString: String;    function Resolve: String;  end;function GreatestCommonDivisor(a, b: Int64): Int64;var  temp: Int64;begin  while b <> 0 do  begin    temp := b;    b := a mod b;    a := temp  end;  result := aend;operator := (I: Int64) F: TFraction;begin  F.Numerator := I;  F.Denominator := 1;end;operator := (S: String) F: TFraction;var  N,D,P: Int64;begin  P := Pos('/',S);  if (P > 0) then  begin    N := StrToint(Copy(S,1,P-1));    D := StrToInt(Copy(S,P+1,MaxInt));    {\$PUSH}{\$WARNINGS OFF}    F.Init(N, D);    {\$POP}  end  else  begin    N := StrToInt(S);    F := N;  end;end;operator + (L: TFraction; R: TFraction) F: TFraction;begin  F.Denominator := L.Denominator * R.Denominator;  F.Numerator := L.Numerator * R.Denominator + R.Numerator * L.Denominator;  F.Normalize;end;operator + (L: TFraction; R: Int64) F: TFraction;var  Temp: TFraction;begin  Temp := R;  F := L + Temp;end;operator + (L: Int64; R: TFraction)F : TFraction;begin  F := R + L;end;operator - (L: TFraction; R: TFraction) F: TFraction;begin  R.Numerator := - R.Numerator;  F := L + R;end;operator - (L: TFraction; R: Int64) F: TFraction;var  Temp: TFraction;begin  Temp := R;  F := L + Temp;end;operator - (L: Int64; R: TFraction) F: TFraction;begin  F := R - L;end;operator * (L: TFraction; R: TFraction) F: TFraction;begin  L.Normalize;  R.Normalize;  F.Numerator := L.Numerator * R.Numerator;  F.Denominator := L.Denominator * R.Denominator;  F.Normalize;end;operator * (L: TFraction; R: Int64) F: TFraction;begin  F := L;  F.Normalize;  F.Numerator := L.Numerator * R;  F.Normalize;end;operator * (L: Int64; R: TFraction) F: TFraction;begin  F := R * L;end;operator / (L: TFraction; R: TFraction) F: TFraction;var  Temp: TFraction;begin  Temp.Numerator := R.Denominator;  Temp.Denominator := R.Numerator;  F := L * Temp;end;operator / (L: TFraction; R: Int64) F: TFraction;begin  F := L;  F.Normalize;  F.Denominator := F.Denominator * R;  F.Normalize;end;operator / (L: Int64; R: TFraction) F: TFraction;begin  F := R / L;end;var  F1, F2, Res: TFraction;{ TFraction }procedure TFraction.Init(ANumerator, ADenominator: Int64);begin  if (ADenominator = 0) then raise EZeroDivide.Create('TFraction: denominator cannot be 0');  if (ADenominator < 0) then  begin    ANumerator := - ANumerator;    ADenominator := - ADenominator;  end;  Numerator := ANumerator;  Denominator := ADenominator;end;procedure TFraction.Normalize;var  GCD: Int64;begin  if (Denominator < 0) then  begin    Numerator := - Numerator;    Denominator := - Denominator;  end;  GCD := GreatestCommonDivisor(Numerator, Denominator);  if (GCD <> 1) then  begin    Numerator := Numerator div GCD;    Denominator := Denominator div GCD  end;end;function TFraction.ToString: String;begin  if (Denominator < 0) then  begin    Numerator := - Numerator;    Denominator := - Denominator;  end;  Result := IntToStr(Numerator) + '/' + IntToStr(Denominator);end;function TFraction.Resolve: String;var  IntPart: Int64;begin  Normalize;  if (Abs(Numerator) > Abs(Denominator)) then  begin    IntPart := Numerator div Denominator;    Numerator := Numerator mod Denominator;    if (IntPart < 0) then Numerator := -Numerator;    if (Numerator <> 0) then      Result := IntToStr(IntPart) + #32 + ToString    else      Result := IntToStr(IntPart);  end  else  begin    Result := ToString;  end;end;begin  F1.Init(2,3);  F2.Init(1,5);  Res := F1 + F2;  writeln(F1.ToString,' + ',F2.ToString,' = ',Res.ToString);  Res := F1 - F2;  writeln(F1.ToString,' - ',F2.ToString,' = ',Res.ToString);  Res := F1 * F2;  writeln(F1.ToString,' * ',F2.ToString,' = ',Res.ToString);  Res := F1 / F2;  writeln(F1.ToString,' / ',F2.ToString,' = ',Res.ToString);  Res := '123/456';  writeln('123/456 -> ',Res.ToString);  Res := '123';  writeln('123 -> ',Res.ToString);  Res := '456/123';  writeln('456/123: ToString = ',Res.ToString,' Resolve = ',Res.Resolve);  Res := '456/-123';  writeln('456/-123: ToString = ',Res.ToString,' Resolve = ',Res.Resolve);end.`
It has an overlaode := operator, so you can do:
Code: [Select]
`  Fraction := '123/456';`
Example output:
Code: [Select]
`C:\Users\Bart\LazarusProjecten\ConsoleProjecten\fractions>fractions2/3 + 1/5 = 13/152/3 - 1/5 = 7/152/3 * 1/5 = 2/152/3 / 1/5 = 10/3123/456 -> 123/456123 -> 123/1456/123: ToString = 456/123 Resolve = 3 29/41456/-123: ToString = -456/123 Resolve = -3 29/4125/5: ToString = 25/5 Resolve = 5`
Bart
« Last Edit: March 22, 2015, 12:45:16 pm by Bart »

#### Bart

• Hero Member
• Posts: 5228
##### Re: Fractions
« Reply #9 on: March 22, 2015, 12:56:09 pm »
...
The main routine needed here is conversion of floats to fractions.

You mean like:
function FloatToFraction(AFloat: Double): TFraction; ?

That is gonna be impossible I guess.
How are you going to test that 2.7182818... is a rational number and not a irrational or transcendental number?

I don't think you were thinking of a trivial solution where you multiply the float by 10^(number of significant digits) and then say that Numerator := Floor(AFloat * 10^(number of significant digits)) and Denominator := 10^(number of significant digits).

Bart

#### typo

• Hero Member
• Posts: 3051
##### Re: Fractions
« Reply #10 on: March 22, 2015, 01:41:55 pm »
Code: [Select]
`program FractionsDemo;{\$mode objfpc}{\$H+}uses  Classes, SysUtils, Fractions;function CalculateFraction(FirstFraction, AOperator, SecondFraction :string): string;var  F1, F2, Res: Fraction;  S1, S2, S3, S4 :string;  p :integer;begin  p := Pos('/', FirstFraction);  S1 := Trim(Copy(FirstFraction, 1, p - 1));  S2 := Trim(Copy(FirstFraction, p + 1, Length(FirstFraction)));  p := Pos('/', SecondFraction);  S3 := Copy(SecondFraction, 1, p - 1);  S4 := Copy(SecondFraction, p + 1, Length(SecondFraction));  F1 := CreateFraction(StrToInt(S1), StrToInt(S2));  F2 := CreateFraction(StrToInt(S3), StrToInt(S4));  case AOperator of  '+': Res := Canonicalize(F1 + F2);  '-': Res := Canonicalize(F1 - F2);  '*': Res := Canonicalize(F1 * F2);  '/': Res := Canonicalize(F1 / F2);  end;     Result := FractionToStr(Res);end;var  Str1, Str2, StrOp :string;begin  WriteLn('Enter first fraction (a/b):');  ReadLn(Str1);  WriteLn('Enter operator (+, -, *, /):');  ReadLn(StrOp);  WriteLn('Enter second fraction (c/d):');  ReadLn(Str2);  WriteLn(CalculateFraction(Str1, StrOp, Str2));  readln;end.                 `
« Last Edit: March 22, 2015, 01:47:59 pm by typo »

#### Bart

• Hero Member
• Posts: 5228
##### Re: Fractions
« Reply #11 on: March 22, 2015, 02:07:06 pm »
Fractions unit + demo:
http://svn.code.sf.net/p/flyingsheep/code/trunk/ConsoleProjecten/fractions/

Compared to HappyLarry's code:
• Supports unitary - operator (F1 := -F2)
• Supports operators =, <, <=, > and >=
• Operator = is overloaded for assigning a string (F1 := '123/456')
• Canonicalize = Normalize (no idea what name is better)
• Has a ToString method
• Has a Resolve method (16/3 -> 5 1/3)
• Slightly faster GreatestCommonDivisor function

Bart
« Last Edit: March 22, 2015, 03:06:57 pm by Bart »

#### wp

• Hero Member
• Posts: 11748
##### Re: Fractions
« Reply #12 on: March 22, 2015, 02:51:31 pm »
Barth, yes, my question was not precise: I mean *formatting* of a float as a fraction, and this does not belong to math but to SysUtils, as an addition to "FormatFloat". I am thinking of a format string such as "# ??/??" which means: "Convert the float to the next fraction with two digits in the denomintator, and split off the integers". This is a rounding operation, such as "FormatFloat('0.00', x)".

There are some Java references to such a function in the internet, but I still have to make sure how they meet the accuracy requirement defined by the count of question marks.

#### Bart

• Hero Member
• Posts: 5228
##### Re: Fractions
« Reply #13 on: March 22, 2015, 03:04:34 pm »
Something like (untested)
Code: [Select]
`  F := 123.456;  Digits := 3;  IntPart := Round(Int(F)) * Round(Exp(10,3));  FracPart := Round(Frac(F) * Round(Exp(10,3)));  S := IntToStr(IntPart) + '/' + IntToStr(FracPart);`
Bart

#### wp

• Hero Member
• Posts: 11748
##### Re: Fractions
« Reply #14 on: March 22, 2015, 03:43:30 pm »
No. Excel converts the number 123.456 to 15432/125 (for format "???/???") or 123 57/125.

I found this simple algorithm (just varying numerator and denominator until the original number is best approximated) in http://stackoverflow.com/questions/95727/how-to-convert-floats-to-human-readable-fractions

Code: [Select]
`procedure FloatAsFraction(AValue: Double; out ANumerator, ADenominator: Integer);const  EPSILON = 1E-6;var  fraction: Double;begin  ANumerator := 1;  ADenominator := 1;  fraction := ANumerator / ADenominator;  while Abs(fraction - AValue) > EPSILON do  begin    if fraction < AValue then      inc(ANumerator)    else begin      inc(ADenominator);      ANumerator := Round(AValue * ADenominator);    end;    fraction := ANumerator / ADenominator;  end;end; `
I think a more efficient way to do it is by means of continued fractions - a good explanation is in http://mathforum.org/library/drmath/view/51886.html, a bit down the page to the posting of Dr Peterson. I'm still searching to find a suitable implementation.