unit GridProcs;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, Grids,
  Globals, DictionaryUnit;

procedure CollectVariableNames(AGrid: TStringGrid; AList: TStrings);

function CollectVecValues(AGrid: TStringGrid; AColIndex: Integer;
  AColCheck: IntDyneVec = nil): DblDyneVec;

function CollectFilteredVecValues(AGrid: TStringGrid; AColIndex, AFilterColIndex: Integer;
  AcceptValue: Double; AColCheck: IntDyneVec = nil): DblDyneVec;

function CollectMatValues(AGrid: TStringGrid; AColIndices: IntDyneVec): DblDyneMat;

function GetColMax(AGrid: TStringGrid; AColIndex: Integer; const AColCheck: IntDyneVec): Double;

procedure GetColMinMax(AGrid: TStringGrid; AColIndex: Integer;
  const AColCheck: IntDyneVec; out AMin, AMax: Double);

function GetVariableIndex(AGrid: TStringGrid; const AVarName: String): Integer;

function GoodRecord(AGrid: TStringGrid; ARow: integer;
  const AColCheck: IntDyneVec): boolean;

function IsEmptyNumericValue(AGrid: TStringGrid; ARow, ACol: Integer): Boolean;

function IsFiltered(AGrid: TStringGrid; ARow: integer): boolean;

function IsMissingValueCode(AGrid: TStringGrid; ARow, ACol: Integer): Boolean;

function IsNumericCol(AColIndex: Integer): Boolean;

function ValidValue(AGrid: TStringGrid; ARow, ACol: integer): boolean;


implementation

uses
  Math;

procedure CollectVariableNames(AGrid: TStringGrid; AList: TStrings);
var
  i: Integer;
begin
  AList.Clear;
  for i := 1 to AGrid.ColCount-1 do
    AList.Add(AGrid.Cells[i, 0]);
end;


{ Extracts the values in the given column from the grid and returns them as an
  array.
  Cells which are filtered or empty are not considered. This check is extended
  over all columns specified by the column indices in AColCheck; AColCheck
  should be empty to consider only the current column.
  Non-numeric values in the considered cell will raise an exception.

  NOTE: AColCheck must not be overdimensioned! }
function CollectVecValues(AGrid: TStringGrid; AColIndex: Integer; AColCheck: IntDyneVec): DblDyneVec;
var
  row, n: Integer;
  val: Double;
begin
  Result := nil;
  SetLength(Result, AGrid.RowCount);
  n := 0;
  for row := 1 to AGrid.RowCount-1 do
  begin
    if Length(AColCheck) = 0 then
    begin
      if not ValidValue(AGrid, row, AColIndex) then continue;
    end else
    begin
      if not GoodRecord(AGrid, row, AColCheck) then continue;
    end;
    if TryStrToFloat(trim(AGrid.Cells[AColIndex, row]), val) then
      Result[n] := val
    else
      raise ELazStats.CreateFmt('Non-numeric string "%s" in column %d, row %d',
        [AGrid.Cells[AColIndex, row], AColIndex, row]);
    inc(n);
  end;
  SetLength(Result, n);
end;


{ Extracts the grid values from the column with index AColIndex, but only those
  for which the value in the same row, but column AFilterCalIndex matches the
  AcceptValue }
function CollectFilteredVecValues(AGrid: TStringGrid; AColIndex, AFilterColIndex: Integer;
  AcceptValue: Double; AColCheck: IntDyneVec = nil): DblDyneVec;
var
  row, n: Integer;
  val1, val2: Double;
begin
  Result := nil;
  SetLength(Result, AGrid.RowCount);
  n := 0;
  for row := 1 to AGrid.RowCount-1 do
  begin
    if Length(AColCheck) = 0 then
    begin
      if not ValidValue(AGrid, row, AColIndex) then continue;
      if not ValidValue(AGrid, row, AFilterColIndex) then continue;
    end else
      if not GoodRecord(AGrid, row, AColCheck) then continue;

    if not TryStrToFloat(trim(AGrid.Cells[AColIndex, row]), val1) then
      raise ELazStats.CreateFmt('Non-numeric string "%s" in column %d, row %d',
        [AGrid.Cells[AColIndex, row]]);
    if not TryStrToFloat(trim(AGrid.Cells[AFilterColIndex, row]), val2) then
      raise ELazStats.CreateFmt('Non-numeric string "%s" in column %d, row %d',
        [AGrid.Cells[AFilterColIndex, row]]);
    if val2 = AcceptValue then
    begin
      Result[n] := val1;
      inc(n);
    end;
  end;
  SetLength(Result, n);
end;


{ Extracts the grid values from the columns with indices given by AColIndices
  and puts them into the columns of the result matrix.
  This means: The result matrix contains the variables as columns and the
  cases as rows.
  "Bad" records (filtered, empty) are skipped. }
function CollectMatValues(AGrid: TStringGrid; AColIndices: IntDyneVec): DblDyneMat;
var
  r, c, i, j: Integer;
  val: Double;
begin
  Result := nil;
  SetLength(Result, AGrid.RowCount, Length(AColIndices));
  i := 0;
  for r:= 1 to AGrid.RowCount-1 do
  begin
    if not GoodRecord(AGrid, r, AColIndices) then Continue;
    for j := 0 to High(AColIndices) do
    begin
      c := AColIndices[j];
      if TryStrToFloat(trim(AGrid.Cells[c, r]), val) then
        Result[i, j] := val
      else
        Result[i, j] := NaN;
    end;
    inc(i);
  end;
  SetLength(Result, i);
end;


function GetColMax(AGrid: TStringGrid; AColIndex: Integer;
  const AColCheck: IntDyneVec): Double;
var
  row: Integer;
  value: Double;
begin
  Result := -Infinity;
  for row := 1 to AGrid.RowCount-1 do
  begin
    if Length(AColCheck) = 0 then
    begin
      if not ValidValue(AGrid, row, AColIndex) then continue;
    end else
    begin
      if not GoodRecord(AGrid, row, AColCheck) then continue;
      end;
    value := StrToFloat(trim(AGrid.Cells[AColIndex, row]));
    if value > Result then
      Result := value;
  end;
end;


{ Determines the minimum and maximum of the values in the specified column of
  the grid. Rows with "invalid" data are ignored. If AColCheck contains other
  column indices these cells must be "valid", too. }
procedure GetColMinMax(AGrid: TStringGrid; AColIndex: Integer;
  const AColCheck: IntDyneVec; out AMin, AMax: Double);
var
  row: Integer;
  value: Double;
begin
  AMin := Infinity;
  AMax := -Infinity;
  for row := 1 to AGrid.RowCount-1 do
  begin
    if Length(AColCheck) = 0 then
    begin
      if not ValidValue(AGrid, row, AColIndex) then continue;
    end else
    begin
      if not GoodRecord(AGrid, row, AColCheck) then continue;
    end;
    value := StrToFloat(trim(AGrid.Cells[AColIndex, row]));
    if value < AMin then AMin := value;
    if value > AMax then AMax := value;
  end;
end;


{ Finds the index of the variable with the specified name among the columns of
  the grid. }
function GetVariableIndex(AGrid: TStringGrid; const AVarName: String): Integer;
begin
  if AVarName <> '' then
    Result := AGrid.Rows[0].IndexOf(AVarName)
  else
    Result := -1;
end;


{ Checks whether all cells specified for the given row in the columns listed in
  the GridPos array are "valid": not filtered and not empty }
function GoodRecord(AGrid: TStringGrid; ARow: integer;
  const AColCheck: IntDyneVec): boolean;
var
  i, j: integer;
begin
  Result := true;
  for i := 0 to High(AColCheck) do
  begin
    j := AColCheck[i];
    if not ValidValue(AGrid, ARow, j) then
      Result := false;
  end;
end;


{ Checks whether the cell in the given row in the given numeric column is empty. }
function IsEmptyNumericValue(AGrid: TStringGrid; ARow, ACol: Integer): Boolean;
var
  value: String;
  isStringField: Boolean;
begin
  value := Trim(AGrid.Cells[ACol, ARow]);
  isStringField := DictionaryFrm.DictGrid.Cells[4, ACol] = 'S';
  Result := not IsStringField and (value = '');
end;


{ Checks whether the specified row is "filtered". Two criteria are needed for
  a row to be filtered:
  - The cell in column FilterCol (global value) must contain the text 'NO'.
  - The global variable "FilterOn" must be TRUE. }
function IsFiltered(AGrid: TStringGrid; ARow: integer): boolean;
begin
  Result := FilterOn and (Trim(AGrid.Cells[FilterCol, ARow]) = 'NO');
end;


{ Checks whether specified cell contains the "missing value code" defined by
  the Dictionary }
function IsMissingValueCode(AGrid: TStringGrid; ARow, ACol: Integer): Boolean;
var
  missingCode: String;
  value: String;
begin
  if ACol < DictionaryFrm.DictGrid.RowCount then
  begin
    missingCode := Trim(DictionaryFrm.DictGrid.Cells[6, ACol]);
    value := Trim(AGrid.Cells[ACol, ARow]);
    Result := (value = missingCode);
  end else
    Result := false;
end;


{ Checks in the dictionary whether the variable in the specified grid column has
  either data type float or integer. }
function IsNumericCol(AColIndex: Integer): Boolean;
var
  typeCode: String;
begin
  if AColIndex < DictionaryFrm.DictGrid.RowCount then
  begin
    typeCode := Trim(DictionaryFrm.DictGrid.Cells[4, AColIndex]);
    Result := (typeCode = 'F') or (typeCode = 'I');
  end else
    Result := false;
end;


{ Checks wheter the value in cell at the given column and row is a not-filtered,
  non-empty number.
  Grid coordinates are in grid units.
  NOTE: non-numeric characters in a numeric field are not taken into account! }
function ValidValue(AGrid: TStringGrid; ARow, ACol: integer): boolean;
begin
  Result := not (
    IsFiltered(AGrid, ARow) or                 // Giltering is active and row is marked to be excluded
    IsEmptyNumericValue(AGrid, ARow, aCol) or  // Column is numeric, but cell is empty
    IsMissingValueCode(AGrid, ARow, ACol)      // Cell contains the "missing value code"
  );
end;


end.