{
 *****************************************************************************
  See the file COPYING.modifiedLGPL.txt, included in this distribution,
  for details about the license.
 *****************************************************************************

  Authors: Alexander Klenin

}

unit SourcesTest;

{$mode objfpc}{$H+}{$R+}

interface

uses
  Classes, SysUtils, FPCUnit, TestRegistry,
  TAChartUtils, TACustomSource, TAIntervalSources, TASources;

type

  { TListSourceTest }

  TListSourceTest = class(TTestCase)
  private
    FSource: TListChartSource;

    procedure AssertItemEquals(
      const AItem: TChartDataItem; AX, AY: Double; AText: String = '';
      AColor: TChartColor = clTAColor);
  protected
    procedure SetUp; override;
    procedure TearDown; override;
  published
    procedure Basic;
    procedure Bounds;
    procedure Cache;
    procedure DataPoint;
    procedure DataPointSeparator;
    procedure Enum;
    procedure Extent;
    procedure Multi;
  end;

  { TRandomSourceTest }

  TRandomSourceTest = class(TTestCase)
  published
    procedure Extent;
  end;

  { TCalculatedSourceTest }

  TCalculatedSourceTest = class(TTestCase)
  private
    FOrigin: TListChartSource;
    FSource: TCalculatedChartSource;
  protected
    procedure SetUp; override;
    procedure TearDown; override;
  published
    procedure Accumulate;
    procedure Derivative;
    procedure Percentage;
    procedure Reorder;
  end;

  TIntervalSourceTest = class(TTestCase)
  private
    procedure AssertValueEquals(
      const AExpected: array of Double; const AActual: TChartValueTextArray);
  published
    procedure IntervalSource;
    procedure ListSource;
  end;

implementation

uses
  Math, TAMath, AssertHelpers;

type
  TDummyTransform = object
  public
    function IdentityDouble(AX: Double): Double;
    function IdentityInteger(AX: Integer): Integer;
    function PrepareValuesInRangeParams: TValuesInRangeParams;
    function Round(AX: Double): Integer;
  end;

var
  VDummyTransform: TDummyTransform;

{ TDummyTransform }

function TDummyTransform.IdentityDouble(AX: Double): Double;
begin
  Result := AX;
end;

function TDummyTransform.IdentityInteger(AX: Integer): Integer;
begin
  Result := AX;
end;

function TDummyTransform.PrepareValuesInRangeParams: TValuesInRangeParams;
begin
  with Result do begin
    FAxisToGraph := @IdentityDouble;
    FGraphToAxis := @IdentityDouble;
    FFormat := '';
    FGraphToImage := @Round;
    FMin := 30;
    FMax := 69;
    FMinStep := 0;
    FScale := @IdentityInteger;
    FUseY := false;
    FIntervals := TChartAxisIntervalParams.Create(nil);
  end;
end;

function TDummyTransform.Round(AX: Double): Integer;
begin
  Result := System.Round(AX);
end;

{ TCalculatedSourceTest }

procedure TCalculatedSourceTest.Accumulate;
var
  i, j: Integer;
  rng: TMWCRandomGenerator;
begin
  FSource.AccumulationMethod := camSum;
  FSource.AccumulationRange := 2;
  AssertEquals(3, FSource.YCount);
  AssertEquals(1, FSource[0]^.X);
  AssertEquals(102, FSource[0]^.Y);
  AssertEquals(2, FSource[1]^.X);
  AssertEquals(102 + 202, FSource[1]^.Y);
  AssertEquals(202 + 302, FSource[2]^.Y);
  FSource.AccumulationDirection := cadForward;
  AssertEquals(202 + 302, FSource[1]^.Y);
  AssertEquals(302 + 402, FSource[2]^.Y);
  FSource.AccumulationDirection := cadBackward;
  FSource.AccumulationMethod := camAverage;
  AssertEquals((2002 + 2102) / 2, FSource[20]^.Y);
  AssertEquals(1, FSource[0]^.X);
  AssertEquals(102, FSource[0]^.Y);
  AssertEquals((102 + 202) / 2, FSource[1]^.Y);
  AssertEquals(102, FSource[0]^.Y);
  FSource.AccumulationDirection := cadCenter;
  AssertEquals((1102 + 1202 + 1302) / 3, FSource[11]^.Y);
  FSource.AccumulationDirection := cadBackward;

  FSource.AccumulationRange := 5;
  rng := TMWCRandomGenerator.Create;
  try
    rng.Seed := 89237634;
    for i := 1 to 100 do begin
      j := rng.GetInRange(5, FSource.Count - 1);
      AssertEquals(IntToStr(j), (j - 1) * 100 + 2, FSource[j]^.Y);
    end;
    FSource.AccumulationRange := 0;
    FSource.AccumulationMethod := camSum;
    rng.Seed := 23784538;
    for i := 1 to 20 do begin
      j := rng.GetInRange(0, FSource.Count - 1);
      AssertEquals(
        IntToStr(j), (j + 1) * (j + 2) * 50 + (j + 1) * 3, FSource[j]^.YList[0]);
    end;
  finally
    rng.Free;
  end;
end;

procedure TCalculatedSourceTest.Derivative;
begin
  FSource.AccumulationMethod := camDerivative;
  FSource.AccumulationRange := 2;
  FOrigin.SetYValue(1, 202);
  AssertTrue(IsNan(FSource[0]^.Y));
  AssertEquals(100, FSource[1]^.Y);
  FSource.AccumulationDirection := cadCenter;
  AssertEquals(100, FSource[0]^.Y);
end;

procedure TCalculatedSourceTest.Percentage;
begin
  FSource.Percentage := true;
  AssertEquals(3, FSource.YCount);
  AssertEquals(102 / (102 + 103 + 104) * 100, FSource[0]^.Y);
  AssertEquals(103 / (102 + 103 + 104) * 100, FSource[0]^.YList[0]);
end;

procedure TCalculatedSourceTest.Reorder;
var
  i: Integer;
begin
  AssertEquals(3, FSource.YCount);
  FSource.ReorderYList := '2';
  AssertEquals(1, FSource.YCount);
  AssertEquals(104, FSource[0]^.Y);
  AssertEquals(204, FSource[1]^.Y);
  FSource.ReorderYList := '0,2';
  AssertEquals(2, FSource.YCount);
  AssertEquals(102, FSource[0]^.Y);
  AssertEquals(104, FSource[0]^.YList[0]);
  AssertEquals(202, FSource[1]^.Y);
  AssertEquals(204, FSource[1]^.YList[0]);
  FSource.ReorderYList := '1,1,1';
  AssertEquals(3, FSource.YCount);
  AssertEquals(103, FSource[0]^.Y);
  AssertEquals([103, 103], FSource[0]^.YList);
  FSource.ReorderYList := '';
  for i := 0 to FSource.Count - 1 do begin
    AssertEquals(FOrigin[i]^.Y, FSource[i]^.Y);
    AssertEquals(FOrigin[i]^.YList, FSource[i]^.YList);
  end;
end;

procedure TCalculatedSourceTest.SetUp;
var
  i: Integer;
begin
  inherited SetUp;
  FOrigin := TListChartSource.Create(nil);
  FSource := TCalculatedChartSource.Create(nil);
  FSource.Origin := FOrigin;
  FOrigin.YCount := 3;
  for i := 1 to 100 do
    FOrigin.SetYList(FOrigin.Add(i, i * 100 + 2), [i * 100 + 3, i * 100 + 4]);
end;

procedure TCalculatedSourceTest.TearDown;
begin
  FreeAndNil(FSource);
  FreeAndNil(FOrigin);
  inherited TearDown;
end;

{ TListSourceTest }

procedure TListSourceTest.AssertItemEquals(
  const AItem: TChartDataItem; AX, AY: Double; AText: String;
  AColor: TChartColor);
begin
  AssertEquals('X', AX, AItem.X);
  AssertEquals('Y', AY, AItem.Y);
  AssertEquals('Text', AText, AItem.Text);
  AssertEquals('Color', AColor, AItem.Color);
end;

procedure TListSourceTest.Basic;
var
  i: Integer;
  srcDest: TListChartSource;
begin
  FSource.Clear;
  AssertEquals(0, FSource.Count);
  AssertEquals(0, FSource.Add(1, 2, 'text', $FFFFFF));
  AssertEquals(1, FSource.Count);
  FSource.Delete(0);
  AssertEquals(0, FSource.Count);
  for i := 1 to 10 do
    FSource.Add(i, i * 2, IntToStr(i));
  srcDest := TListChartSource.Create(nil);
  try
    srcDest.CopyFrom(FSource);
    AssertEquals(FSource.Count, srcDest.Count);
    for i := 0 to FSource.Count - 1 do
      with FSource[i]^ do
        AssertItemEquals(srcDest[i]^, X, Y, Text, Color);
  finally
    srcDest.Free;
  end;
end;

procedure TListSourceTest.Bounds;

  procedure Check2(AExpectedLB, AExpectedUB: Integer; AXMin, AXMax: Double);
  var
    lb, ub: Integer;
  begin
    FSource.FindBounds(AXMin, AXMax, lb, ub);
    AssertEquals(AExpectedLB, lb);
    AssertEquals(AExpectedUB, ub);
  end;

  procedure Check(AExpectedLB, AExpectedUB: Integer; AValue: Double);
  begin
    Check2(AExpectedLB, AExpectedUB, AValue, AValue)
  end;

  procedure CheckAll;
  begin
    Check(1, 1, 2);
    Check(1, 0, 1.9);
    Check(0, -1, 0.9);
    Check(5, 4, 5.1);
    Check(4, 3, 4.9);
    Check2(2, 4, 3, 1e100);
    Check2(0, 1, -1e100, 2);
  end;

begin
  FSource.Clear;
  FSource.Add(1, 2);
  FSource.Add(2, 3);
  FSource.Add(3, 4);
  FSource.Add(4, 5);
  FSource.Add(5, 6);
  FSource.Sorted := true;
  CheckAll;
  FSource.Sorted := false;
  CheckAll;
  FSource.SetXValue(1, SafeNan);
  Check(2, 0, 2);
  FSource.SetXValue(0, SafeNan);
  Check2(2, 2, -1e100, 3);
  Check2(2, 2, NegInfinity, 3);
end;

procedure TListSourceTest.Cache;
begin
  FSource.Clear;
  FSource.Add(5, 6);
  FSource.Add(7, 8);
  AssertEquals(14, FSource.ValuesTotal);
  FSource.Add(8, SafeNan);
  AssertEquals(14, FSource.ValuesTotal);
  FSource.Delete(2);
  AssertEquals(14, FSource.ValuesTotal);
  FSource.Delete(1);
  AssertEquals(6, FSource.ValuesTotal);
  FSource.SetYValue(0, SafeNan);
  AssertEquals(0, FSource.ValuesTotal);
  FSource.SetYValue(0, 5);
  AssertEquals(5, FSource.ValuesTotal);

  FSource.Clear;
  AssertEquals(0, FSource.ValuesTotal);
  FSource.Add(NaN, NaN);
  FSource.BeginUpdate;
  FSource.EndUpdate;
  AssertEquals(0, FSource.ValuesTotal);
end;

procedure TListSourceTest.DataPoint;
begin
  FSource.Clear;
  FSource.DataPoints.Add('3|4|?|text1');
  FSource.DataPoints.Add('5|6|$FF0000|');
  AssertEquals(2, FSource.Count);
  AssertItemEquals(FSource[0]^, 3, 4, 'text1');
  AssertItemEquals(FSource[1]^, 5, 6, '', $FF0000);
  FSource[0]^.Color := 0;
  AssertEquals('3|4|$000000|text1', FSource.DataPoints[0]);
  FSource.DataPoints.Add('7|8|0|two words');
  AssertEquals('two words', FSource[2]^.Text);
end;

procedure TListSourceTest.DataPointSeparator;
var
  oldSeparator: Char;
begin
  FSource.Clear;
  oldSeparator := DefaultFormatSettings.DecimalSeparator;
  try
    DefaultFormatSettings.DecimalSeparator := ':';
    FSource.DataPoints.Add('3:5');
    AssertEquals(3.5, FSource[0]^.X);
    FSource.DataPoints[0] := '4.5';
    AssertEquals(4.5, FSource[0]^.X);
  finally
    DefaultFormatSettings.DecimalSeparator := oldSeparator;
  end;
end;

procedure TListSourceTest.Enum;
var
  it: PChartDataItem;
  s: Double = 0;
begin
  FSource.Clear;
  for it in FSource do
    s += 1;
  AssertEquals(0, s);
  FSource.Add(10, 1);
  FSource.Add(20, 7);
  for it in FSource do
    s += it^.X + it^.Y;
  AssertEquals(38, s);
end;

procedure TListSourceTest.Extent;

  procedure AssertExtent(AX1, AY1, AX2, AY2: Double);
  begin
    with FSource.Extent do begin
      AssertEquals('X1', AX1, a.X);
      AssertEquals('Y1', AY1, a.Y);
      AssertEquals('X2', AX2, b.X);
      AssertEquals('Y2', AY2, b.Y);
    end;
  end;

begin
  FSource.Clear;
  Assert(IsInfinite(FSource.Extent.a.X) and IsInfinite(FSource.Extent.a.Y));
  Assert(IsInfinite(FSource.Extent.b.X) and IsInfinite(FSource.Extent.b.Y));

  FSource.Add(1, 2);
  AssertExtent(1, 2, 1, 2);

  FSource.Add(3, 4);
  AssertExtent(1, 2, 3, 4);

  FSource.SetXValue(0, -1);
  AssertExtent(-1, 2, 3, 4);

  FSource.SetXValue(1, -2);
  AssertExtent(-2, 2, -1, 4);

  FSource.SetXValue(1, SafeNaN);
  AssertExtent(-1, 2, -1, 4);
  FSource.SetXValue(1, -2);

  FSource.SetYValue(0, 5);
  AssertExtent(-2, 4, -1, 5);

  FSource.SetYValue(0, 4.5);
  AssertExtent(-2, 4, -1, 4.5);

  FSource.SetYValue(1, SafeNaN);
  AssertExtent(-2, 4.5, -1, 4.5);

  FSource.Delete(1);
  AssertExtent(-1, 4.5, -1, 4.5);

  FSource.Clear;
  FSource.Add(1, 1);
  FSource.Add(2, 2);
  FSource.Add(3, 3);
  FSource.Add(4, 4);
  FSource.Delete(0);
  FSource.Delete(1);
  AssertExtent(2, 2, 4, 4);
end;

procedure TListSourceTest.Multi;
begin
  FSource.Clear;
  AssertEquals(1, FSource.YCount);
  FSource.Add(1, 2);
  FSource.YCount := 2;
  AssertEquals([0], FSource[0]^.YList);
  FSource.SetYList(0, [3, 4]);
  AssertEquals('Extra items are chopped', [3], FSource[0]^.YList);
  FSource.DataPoints.Add('1|2|3|4|?|t');
  AssertEquals(3, FSource.YCount);
  AssertEquals(2, FSource[1]^.Y);
  AssertEquals([3, 4], FSource[1]^.YList);

  FSource.AddXYList(2, [7, 8, 9]);
  AssertEquals(3, FSource.YCount);
  AssertEquals(7, FSource[2]^.Y);
  AssertEquals([8, 9], FSource[2]^.YList);
  FSource.AddXYList(3, [10]);
  AssertEquals(4, FSource.Count);
  AssertEquals(3, FSource.YCount);
  AssertEquals(10, FSource[3]^.Y);
  AssertEquals([0, 0], FSource[3]^.YList);
  try
    FSource.AddXYList(4, []);
    Fail('Empty YList');
  except on E: Exception do
    AssertTrue('Empty YList', E is TListChartSource.EYListEmptyError);
  end;
end;

procedure TListSourceTest.SetUp;
begin
  inherited SetUp;
  FSource := TListChartSource.Create(nil);
end;

procedure TListSourceTest.TearDown;
begin
  FreeAndNil(FSource);
  inherited TearDown;
end;

{ TRandomSourceTest }

procedure TRandomSourceTest.Extent;
var
  s: TRandomChartSource;
  ext: TDoubleRect;
begin
  s := TRandomChartSource.Create(nil);
  try
    s.XMin := 10;
    s.XMax := 20;
    s.YMin := 5;
    s.YMax := 6;
    s.PointsNumber := 1000;
    ext := s.Extent;
    AssertEquals(10, ext.a.X);
    AssertEquals(20, ext.b.X);
    Assert(ext.a.Y > 5);
    Assert(ext.b.Y < 6);
    Assert(ext.a.Y < ext.b.Y);
  finally
    s.Free;
  end;
end;

{ TIntervalSourceTest }

procedure TIntervalSourceTest.AssertValueEquals(
  const AExpected: array of Double; const AActual: TChartValueTextArray);
var
  i: Integer;
  a: array of Double;
begin
  SetLength(a, Length(AActual));
  for i := 0 to High(AActual) do
    a[i] := AActual[i].FValue;
  AssertEquals(AExpected, a, 1e-6);
end;

procedure TIntervalSourceTest.IntervalSource;
var
  p: TValuesInRangeParams;
  src: TIntervalChartSource;
  r: TChartValueTextArray = nil;
begin
  p := VDummyTransform.PrepareValuesInRangeParams;
  src := TIntervalChartSource.Create(nil);
  try
    src.Params.MaxLength := 15;
    src.ValuesInRange(p, r);
    AssertValueEquals([20, 30, 40, 50, 60, 70], r);
    src.Params.Options := [aipUseCount];
    src.Params.Count := 7;
    src.Params.Tolerance := 1;
    src.ValuesInRange(p, r);
    AssertValueEquals([24, 30, 36, 41, 47, 52, 58, 63, 69, 75], r);
  finally
    p.FIntervals.Free;
    src.Free;
  end;
end;

procedure TIntervalSourceTest.ListSource;
var
  i: Integer;
  p: TValuesInRangeParams;
  r: TChartValueTextArray = nil;
  src: TListChartSource;

  procedure Check(const AExpected: array of Double);
  begin
    r := nil;
    src.ValuesInRange(p, r);
    AssertValueEquals(AExpected, r);
  end;

begin
  p := VDummyTransform.PrepareValuesInRangeParams;
  p.FFormat := '%4:g';
  src := TListChartSource.Create(nil);
  for i := 1 to 10 do
    src.Add(10 * i, i);
  try
    Check([20, 30, 40, 50, 60, 70]);
    p.FIntervals.MinLength := 20;
    Check([20, 30, 50, 70]);
    p.FMin := 81;
    p.FMax := 82;
    Check([80, 90]);
    p.FMin := 9;
    p.FMax := 11;
    Check([10, 20]);
    src.Add(8, 11);
    Check([8, 10, 20]);
    p.FMin := 1;
    p.FMax := 20;
    p.FIntervals.Options := p.FIntervals.Options - [aipUseMinLength];
    Check([8, 10, 20, 30]);
    p.FIntervals.Options := p.FIntervals.Options + [aipUseMinLength];
    p.FMax := 50;
    Check([8, 30, 50, 60]);
    AssertEquals('Lower bound not first in-range value', '8', r[0].FText);
    src.Sort;
    Check([8, 30, 50, 60]);
    p.FIntervals.Tolerance := 3;
    Check([10, 30, 50, 60]);
  finally
    p.FIntervals.Free;
    src.Free;
  end;
end;

initialization

  RegisterTests([
    TListSourceTest, TRandomSourceTest, TCalculatedSourceTest,
    TIntervalSourceTest]);

end.

