unit fetUtils;

{$IFDEF FPC}
 {$mode delphi} //objfpc}{$H+}
{$ENDIF}

interface

uses
  Classes, SysUtils,
 {$IFDEF FPC}
  fpcunit, testutils, testregistry;
 {$ELSE}
  TestFrameWork;
 {$ENDIF}

type
  TstUtils = class(TTestCase)
  published
    procedure TestCountChar;
    procedure TestFloatToRational;
    procedure TestInsertSpaces;
    procedure TestLookup;
    procedure TestSplit;
    procedure TestSplitGPS;
    procedure TestStrToGPS;
    procedure TestStrToRational;
  end;

implementation

uses
  math,
  fpeGlobal, fpeUtils;

type
  TCountCharParam = record
    TestString: String;
    ch: Char;
    Count: Integer;
  end;

  TInsertSpacesParam = record
    TestString: String;
    ResultString: String;
  end;

  TFloatToRationalParam = record
    Value, Precision: Double;
    Num, Denom: Integer;
    Error: Integer;  // 0:ok, 1: Num is wrong, 2: Denom is wrong
  end;

  TLookupParam = record
    SearchForKey: Boolean;
    SearchStr: String;
    ResultStr: String;
  end;

  TSplitGpsParam = record
    Value: Double;
    Degs: Double;
    Mins: Double;
    Secs: Double
  end;

  TSplitParam = record
    Text: String;
    Sep: String;
    NumParts: Integer;
    Parts: Array[0..2] of string;
  end;

  TStrToGpsParam = record
    Text: String;
    Degs: Double;
    Valid: Boolean;
  end;

  TStrToRationalParam = record
    Value: String;
    Num, Denom: Integer;
  end;

const
  CountCharParams: array[0..5] of TCountCharParam = (
    (TestString:'';   ch:'a'; Count:0),
    (TestString:'a';  ch:'a'; Count:1),
    (TestString:'aa'; ch:'a'; Count:2),
    (TestString:'b';  ch:'a'; Count:0),
    (TestString:'ab'; ch:'a'; Count:1),
    (TestString:'ba'; ch:'a'; Count:1)
  );

  InsertSpacesParams: array[0..14] of TInsertSpacesParam = (
    (TestString: 'Artist';                ResultString: 'Artist'),
    (TestString: 'ShutterSpeed';          ResultString: 'Shutter Speed'),
    (TestString: 'ThumbnailXResolution';  ResultString: 'Thumbnail X Resolution'),
    (TestString: 'YCbCrPositioning';      ResultString: 'Y Cb Cr Positioning'),
    (TestString: 'ISO';                   ResultString: 'ISO'),
    (TestString: 'GPSInfo';               ResultString: 'GPS Info'),
    (TestString: 'IPTC/NAA';              ResultString: 'IPTC/NAA'),
    (TestString: 'XPTitle';               ResultString: 'XP Title'),
    (TestString: 'PrintIM';               ResultString: 'Print IM'),
    (TestString: 'ResolutionX';           ResultString: 'Resolution X'),
    (TestString: 'XResolution';           ResultString: 'X Resolution'),
    (TestString: 'CCD ISO';               ResultString: 'CCD ISO'),
    (TestString: 'AE setting';            ResultString: 'AE setting'),
    (TestString: 'abc ABC';               ResultString: 'abc ABC'),
    (TestString: 'abc Abc';               ResultString: 'abc Abc')
  );

  FloatToRationalParams: array[0..8] of TFloatToRationalParam = (
    (Value:0.0;         Precision: 1E-6; Num:0; Denom:1;     Error:0),    // 0
    (Value:1.0;         Precision: 1E-6; Num:1; Denom:1;     Error:0),    // 1
    (Value:0.5;         Precision: 1E-6; Num:1; Denom:2;     Error:0),    // 2
    (Value:0.01;        Precision: 1E-6; Num:1; Denom:100;   Error:0),    // 3
    (Value:0.333333333; Precision: 1E-6; Num:1; Denom:3;     Error:0),    // 4
    (value:1.166666667; Precision: 1E-6; Num:7; Denom:6;     Error:0),    // 5
    (Value:NaN;         Precision: 1E-6; Num:1; Denom:0;     Error:0),    // 6
    (Value:0.3333;      Precision: 1E-6; Num:1; Denom:3;     Error:2),    // 7
    (Value:0.1;         Precision: 1E-6; Num:1; Denom:3;     Error:2)     // 8
  );

  LkupTbl: String = '0:Zero,1:One,2:Two';
  LookupParams: array[0..8] of TLookupParam = (
    (SearchForKey:true;  SearchStr:'0';     ResultStr:'Zero'),
    (SearchForKey:true;  SearchStr:'1';     ResultStr:'One'),
    (SearchForKey:true;  SearchStr:'2';     ResultStr:'Two'),
    (SearchForKey:true;  SearchStr:'$2';    ResultStr:'Two'),
    (SearchForKey:true;  SearchStr:'3';     ResultStr:'3'),
    (SearchForKey:false; SearchStr:'Zero';  ResultStr:'0'),
    (SearchForKey:false; SearchStr:'One';   ResultStr:'1'),
    (SearchForKey:false; SearchStr:'Two';   ResultStr:'2'),
    (SearchForKey:false; SearchStr:'Three'; ResultStr:'')
  );

  SplitGpsParams: array[0..3] of TSplitGpsParam = (
    (Value:0.5;              Degs: 0; Mins:30; Secs: 0),
    (Value:2.777777E-4;      Degs: 0; Mins: 0; Secs: 1),
    (Value:50.2527777777777; Degs:50; Mins:15; Secs:10),
    (Value:50.2583333333333; Degs:50; Mins:15; Secs:30)
  );

  SplitParams: array[0..3] of TSplitParam = (
    (Text:'One';        Sep: ';';  NumParts: 1; Parts:('One', '',    '')),
    (Text:'One,Two';    Sep: ',';  NumParts: 2; Parts:('One', 'Two', '')),
    (Text:'One, Two';   Sep: ', '; NumParts: 2; Parts:('One', 'Two', '')),
    (Text:'One'#0'Two'; Sep: #0;   NumParts: 2; Parts:('One', 'Two', ''))
  );

  // 1/3600 = 2.77777777777E-4,  1/60 = 0,01666666666666667
  StrToGpsParams: array[0..11] of TStrToGpsParam = (
    (Text:'0 deg 30'' 0"';     Degs: 0.5;              Valid: true),
    (Text:'0 deg  0'' 1"';     Degs: 2.777777E-4;      Valid: true),
    (Text:'50 deg 15'' 10"';   Degs: 50.2527777777777; Valid: true),
    (Text:'50 deg 15'' 30"';   Degs: 50.2583333333333; Valid: true),
    (Text:'50 deg 15.5''';     Degs: 50.2583333333333; Valid: true),
    (Text:'50 deg 60'' 30"';   Degs: NaN;              Valid: false),
    (Text:'50 deg 15'' 70"';   Degs: NaN;              Valid: false),
    (Text:'50.1° 15'' 70"';    Degs: NaN;              Valid: false),
    (Text:'50 deg 15.3'' 50"'; Degs: NaN;              Valid: false),
    (Text:'50 deg -15'' 50"';  Degs: NaN;              Valid: false),
    (Text:'50 deg 15'' -50"';  Degs: NaN;              Valid: false),
    (Text:'-50 deg 15'' 30"';  Degs: 50.2583333333333; Valid: true)
  );

  StrToRationalParams: array[0..9] of TStrToRationalParam = (
    (Value:'0';         Num:0; Denom:1),     // 0
    (Value:'1';         Num:1; Denom:1),     // 1
    (Value:'1/2';       Num:1; Denom:2),     // 2
    (Value:'1/ 2';      Num:1; Denom:2),     // 3
    (Value:'1 /2';      Num:1; Denom:2),     // 4
    (Value:'1 / 2';     Num:1; denom:2),     // 5
    (Value:' 1/2';      Num:1; Denom:2),     // 6
    (Value:'1/2 ';      Num:1; Denom:2),     // 7
    (Value:' 1/2 ';     Num:1; Denom:2),     // 8
    (value:'';          Num:1; Denom:0)      // 9
  );

procedure TstUtils.TestCountChar;
var
  currCount: Integer;
  i: Integer;
begin
  for i:=Low(CountCharParams) to High(CountCharParams) do begin
    currCount := CountChar(CountCharParams[i].ch, CountCharParams[i].TestString);
    CheckEquals(CountCharParams[i].Count, currCount,
      'CountChar mismatch, test case ' + IntToStr(i));
  end;
end;

procedure TstUtils.TestFloatToRational;
var
  currR: TExifRational;
  i: Integer;
begin
  for i:=Low(FloatToRationalParams) to High(FloatToRationalParams) do
    with FloatToRationalParams[i] do begin
      currR := FloatToRational(Value, Precision);
      case Error of
        0: begin
             CheckEquals(currR.Numerator, Num,
               'FloatToRational numerator mismatch, test case ' + IntToStr(i));
             CheckEquals(currR.Denominator, Denom,
               'FloatToRational denominator mismatch, test case ' + IntToStr(i));
           end;
        1: CheckNotEquals(currR.Numerator, Num,
             'Unexpected FloatToRational numerator match, test case ' + IntToStr(i));
        2: CheckNotEquals(currR.Denominator, Denom,
             'Unexpected FloatToRational denominator match, test case ' + IntToStr(i));
      end;
    end;
end;

procedure TstUtils.TestInsertSpaces;
var
  currStr: String;
  i: Integer;
begin
  for i:=Low(InsertSpacesParams) to High(InsertSpacesParams) do begin
    currStr := InsertSpaces(InsertSpacesParams[i].TestString);
    CheckEquals(InsertSpacesParams[i].ResultString, currStr,
      'InsertSpaces mismatch, test case ' + IntToStr(i));
  end;
end;

function SameIntegerKey(AKey1, AKey2: String): Boolean;
var
  k1, k2: Integer;
begin
  Result := TryStrToInt(AKey1, k1) and TryStrToInt(AKey2, k2) and (k1 = k2);
end;

function SameStringKey(AKey1, AKey2: String): Boolean;
begin
  Result := SameText(AKey1, AKey2);
end;

procedure TstUtils.TestLookup;
var
  currResult: String;
  i: Integer;
begin
  for i:=Low(LookupParams) to High(LookupParams) do
    with LookupParams[i] do begin
      if SearchForKey then
        currResult := LookupValue(SearchStr, LkupTbl, @SameIntegerKey)
      else
        currResult := LookupKey(SearchStr, LkupTbl, @SameStringKey);
    CheckEquals(ResultStr, currResult,
      'Lookup mismatch, test case ' + IntToStr(i));
  end;
end;

procedure TstUtils.TestSplit;
var
  currResult: TStringArray;
  i, j: Integer;
begin
  for i:=Low(SplitParams) to High(SplitParams) do
    with SplitParams[i] do begin
      currResult := Split(Text, Sep);
      CheckEquals(NumParts, Length(currResult), 'Split count mismatch');
      for j:=0 to NumParts-1 do
        CheckEquals(Parts[j], currResult[j], 'Split mismatch in array element #' + IntToStr(j));
    end;
end;

procedure TstUtils.TestSplitGPS;
const
  EPS = 1E-6;
var
  currDeg, currMin, currSec: Double;
  i: Integer;
begin
  for i:=Low(SplitGPSParams) to High(SplitGPSParams) do
    with SplitGPSParams[i] do begin
      SplitGPS(Value, currDeg, currMin, currSec);
      CheckEquals(Degs, currDeg, EPS, 'Degree value mismatch, test case ' + IntToStr(i));
      CheckEquals(Mins, currMin, EPS, 'Minutes mismatch, test case ' + IntToStr(i));
      CheckEquals(Secs, currSec, EPS, 'Seconds value mismatch, test case ' + IntToStr(i));
    end;
end;

procedure TstUtils.TestStrToGPS;
const
  EPS = 1E-8;
var
  currDeg: Double;
  i: Integer;
  currOK: Boolean;
begin
  for i:=Low(StrToGpsParams) to High(StrToGpsParams) do begin
    with StrToGpsParams[i] do begin
      currOK := TryStrToGps(Text, currDeg);
      CheckEquals(Valid, currOK, 'GPS result validity mismatch, test case ' + IntToStr(i));
      if Valid then
        CheckEquals(Degs, currDeg, EPS, 'GPS degress mismatch, test case ' + IntToStr(i));
    end;
  end;
end;

procedure TstUtils.TestStrToRational;
var
  currR: TExifRational;
  i: Integer;
begin
  for i:=Low(StrToRationalParams) to High(StrToRationalParams) do
    with StrToRationalParams[i] do begin
      currR := StrToRational(Value);
      CheckEquals(currR.Numerator, Num,
        'StrToRational numerator mismatch, test case ' + IntToStr(i));
      CheckEquals(currR.Denominator, Denom,
        'StrToRational denominator mismatch, test case ' + IntToStr(i));
    end;
end;

initialization
 {$IFDEF FPC}
  RegisterTest(TstUtils);
 {$ELSE}
  TestFramework.RegisterTest(TstUtils.Suite);
 {$ENDIF}

end.