Recent

Author Topic: fpspreadsheet (Spready) - recalculate only selected cell's formula  (Read 864 times)

tatamata

  • Hero Member
  • *****
  • Posts: 804
    • ZMSQL - SQL enhanced in-memory database
In modified spready I have registered a custom function, which is very long running.
Therefore, I want to manually control triggering of calculation of selected cell containing the formula.
Is it possible to force calculation of selected cell only, while suppressing of recalculation of all other cells?
So far, whatever I did, it always ends up in recalculating the whole sheet...

wp

  • Hero Member
  • *****
  • Posts: 12696
Re: fpspreadsheet (Spready) - recalculate only selected cell's formula
« Reply #1 on: February 11, 2025, 11:49:29 pm »
To only way to be sure that only a specific cell is recalculated is when it contains the only formula cell in the workbook. That's what I am doing when debugging formulas.

tatamata

  • Hero Member
  • *****
  • Posts: 804
    • ZMSQL - SQL enhanced in-memory database
Re: fpspreadsheet (Spready) - recalculate only selected cell's formula
« Reply #2 on: February 12, 2025, 01:34:00 am »
Not sure that I understood you. Do you want to say that it is not possible?

So far I have tried so many approaches that I am sick. My current attempt:
Code: Pascal  [Select][+][-]
  1. procedure TMainForm.RecalculateSelectedRange(Selection: TRect);
  2. var
  3.   sheet: TsWorksheet;
  4.   row, col: Integer;
  5.   ACell: PCell;
  6.   OrigFormula: string;
  7.   rpn: TsRPNFormula;
  8.   SavedOptions: TsWorkbookOptions;
  9.   WasDeferred: Boolean;
  10. begin
  11.   sheet := WorkbookSource.Worksheet;
  12.   if sheet = nil then
  13.   begin
  14.     ShowMessage('Error: Worksheet is not assigned.');
  15.     Exit;
  16.   end;
  17.  
  18.   // Save current options and deferral state.
  19.   SavedOptions := WorkbookSource.Workbook.Options;
  20.   WasDeferred := DeferFormulaCalculation;
  21.   try
  22.     // Temporarily disable automatic recalculation for a targeted update.
  23.     WorkbookSource.Workbook.Options := SavedOptions - [boAutoCalc, boCalcBeforeSaving];
  24.     DeferFormulaCalculation := False;
  25.  
  26.     for row := Selection.Top - 1 to Selection.Bottom - 1 do
  27.       for col := Selection.Left - 1 to Selection.Right - 1 do
  28.       begin
  29.         // Check if the coordinates are within bounds.
  30.         if (row < 0) or (row > sheet.GetLastRowIndex) or (col < 0) or (col > sheet.GetLastColIndex) then
  31.           Continue;
  32.  
  33.         ACell := sheet.FindCell(row, col);
  34.         if Assigned(ACell) and HasFormula(ACell) then
  35.         begin
  36.           // Get the stored formula text.
  37.           OrigFormula := sheet.ReadFormulaAsString(ACell, True);
  38.           if OrigFormula <> '' then
  39.           begin
  40.             // Rewrite the formula to force a rebuild of the cell's internal structure.
  41.             ACell := sheet.WriteFormula(row, col, OrigFormula);
  42.             if Assigned(ACell) then
  43.             begin
  44.               // Rebuild the RPN formula for the cell.
  45.               rpn := sheet.BuildRPNFormula(ACell);
  46.               if Length(rpn) > 0 then
  47.                 // Call CalcFormula with a pointer to the first element.
  48.                 sheet.CalcFormula(@rpn[0]);
  49.             end;
  50.           end
  51.           else
  52.             ShowMessage(Format('Error: No stored formula found for cell (%d, %d).', [row, col]));
  53.         end;
  54.       end;
  55.     WorksheetGrid.InvalidateRange(Selection);
  56.   except
  57.     on E: Exception do
  58.       ShowMessage('Error recalculating range: ' + E.Message);
  59.   end;
  60.  
  61.   // Restore original workbook options and deferral flag.
  62.   WorkbookSource.Workbook.Options := SavedOptions;
  63.   DeferFormulaCalculation := WasDeferred;
  64. end;
  65.  
  66. procedure TMainForm.AcRecalculateSelectedCellExecute(Sender: TObject);
  67. var
  68.   Selection: TRect;
  69. begin
  70.   // Create a one-cell selection at the active cell.
  71.   Selection := Rect(WorksheetGrid.Col, WorksheetGrid.Row, WorksheetGrid.Col, WorksheetGrid.Row);
  72.   RecalculateSelectedRange(Selection);
  73. end;
  74.  
  75. procedure TMainForm.AcRecalculateSelectedRangeExecute(Sender: TObject);
  76. begin
  77.   RecalculateSelectedRange(WorksheetGrid.Selection);
  78. end;  
  79.  

In this version I get: "Error recalculating range: Access violation". I always get either that or all cells with formulas are calculated, which is not what I want.
Is it even possible to succeed, should I quit?
« Last Edit: February 12, 2025, 01:37:38 am by tatamata »

wp

  • Hero Member
  • *****
  • Posts: 12696
Re: fpspreadsheet (Spready) - recalculate only selected cell's formula
« Reply #3 on: February 12, 2025, 03:53:51 pm »
I don't understand what you are doing. But the way to calculate a single formula is the call TsWorksheet.CalcFormula(AFormula) where AFormula is the parsed formula which is stored in the workbook's FFormulas. You can find it for a given cell at row R and column C by calling Workbook.Formulas.FindFormula(R, C).
Code: Pascal  [Select][+][-]
  1. var
  2.   f: PsFormula;
  3. begin
  4.   ...
  5.   f := Workbook.Formulas.FindFormula(R, C);
  6.   if Assigned(f) then
  7.     Worksheet.CalcFormula(f);
  8. ...
But be aware that if the formula arguments depend on other cells which contain formulas themselves these formulas will be calculated as well. Otherwise that entire exercise does not make any sense. I cannot guarantee whether formulas not related to this kind of tree are not calculated either - the FPSpreadsheet formula engine is quite (probably: too) simple.

tatamata

  • Hero Member
  • *****
  • Posts: 804
    • ZMSQL - SQL enhanced in-memory database
Re: fpspreadsheet (Spready) - recalculate only selected cell's formula
« Reply #4 on: February 13, 2025, 08:58:19 am »
Thank you, this helped me to write final solution, how to manually (re)calculate only selected cells range, containing deferred long-running formula:
Code: Pascal  [Select][+][-]
  1. //Added by Zlatko Matić
  2. procedure TMainForm.AcRecalculateFormulasExecute(Sender: TObject);
  3. var
  4.   WasDeferred: Boolean;
  5.   SavedOptions: TsWorkbookOptions;
  6.   OriginalCursor: TCursor; // Variable to store the original cursor
  7.   StartTime, EndTime: Int64;
  8. begin
  9.   OriginalCursor := Screen.Cursor; // Save the current cursor
  10.   Screen.Cursor := crHourglass;    // Change to hourglass cursor
  11.   Application.ProcessMessages;
  12.   StartTime := GetHighResolutionTime;
  13.   try
  14.     // Save current workbook options and deferral flag.
  15.     SavedOptions := WorkbookSource.Workbook.Options;
  16.     WasDeferred := DeferFormulaCalculation;
  17.  
  18.     if WasDeferred then
  19.     begin
  20.       DeferFormulaCalculation := False;
  21.       AcSettingsDeferCalculation.Checked := False;
  22.       WorkbookSource.Workbook.Options := WorkbookSource.Workbook.Options + [boAutoCalc];
  23.     end;
  24.  
  25.     try
  26.       RecalculateFormulas; // Perform recalculation logic here...
  27.     finally
  28.       // Restore the original workbook options and deferral state.
  29.       DeferFormulaCalculation := WasDeferred;
  30.       WorkbookSource.Workbook.Options := SavedOptions;
  31.       AcSettingsDeferCalculation.Checked := WasDeferred;
  32.     end;
  33.  
  34.   finally
  35.     EndTime := GetHighResolutionTime; // Capture end time
  36.     Screen.Cursor := OriginalCursor; // Restore the original cursor
  37.     Application.ProcessMessages;
  38.  
  39.     // Show completion message with flexible time formatting
  40.     ShowMessage(Format('(Re)calculation completed in %s.', [FormatElapsedTime(EndTime - StartTime)]));
  41.   end;
  42. end;
  43.  
  44.  
  45.  
  46. //------------------------------------------------------------------------
  47. // MANUALLY RECALCULATE SELECTED CELL OR RANGE (Added by Zlatko Matić)
  48. //------------------------------------------------------------------------
  49.  
  50. procedure TMainForm.RecalculateSelectedRange(Selection: TRect);
  51. var
  52.   sheet: TsWorksheet;
  53.   row, col: Integer;
  54.   ACell: PCell;
  55.   SavedOptions: TsWorkbookOptions;
  56.   WasDeferred: Boolean;
  57.   OriginalCursor: TCursor;
  58.   StartTime, EndTime: Int64;
  59.   f: PsFormula;  // local variable for the parsed formula pointer
  60. begin
  61.   OriginalCursor := Screen.Cursor;
  62.   Screen.Cursor := crHourglass;
  63.   Application.ProcessMessages;
  64.   StartTime := GetHighResolutionTime;
  65.  
  66.   try
  67.  
  68.     sheet := WorkbookSource.Worksheet;
  69.     if sheet = nil then
  70.     begin
  71.       ShowMessage('Error: Worksheet is not assigned.');
  72.       Exit;
  73.     end;
  74.  
  75.     // Save current options and deferral state.
  76.     SavedOptions := WorkbookSource.Workbook.Options;
  77.     WasDeferred := DeferFormulaCalculation;
  78.     try
  79.       // Temporarily disable automatic recalculation for a targeted update.
  80.       WorkbookSource.Workbook.Options := SavedOptions - [boAutoCalc, boCalcBeforeSaving];
  81.       DeferFormulaCalculation := False;
  82.  
  83.       for row := Selection.Top - 1 to Selection.Bottom - 1 do
  84.         for col := Selection.Left - 1 to Selection.Right - 1 do
  85.         begin
  86.           // Check if the coordinates are within bounds.
  87.           if (row < 0) or (row > sheet.GetLastRowIndex) or (col < 0) or (col > sheet.GetLastColIndex) then
  88.             Continue;
  89.  
  90.           ACell := sheet.FindCell(row, col);
  91.           //if Assigned(ACell) and (ACell^.ContentType = cctFormula) then  // does not work!
  92.           if Assigned(ACell) and (cfHasFormula in ACell^.Flags) then
  93.           begin
  94.             f := sheet.Formulas.FindFormula(row, col);
  95.             if Assigned(f) then  begin
  96.               //ShowMessage(Format('Parsed formula found for cell (%d, %d): %p', [row, col, f]));
  97.               sheet.CalcFormula(f);
  98.             end
  99.             else
  100.               ShowMessage(Format('Error: No parsed formula found for cell (%d, %d).', [row, col]));
  101.           end;
  102.         end;
  103.       WorksheetGrid.InvalidateRange(Selection);
  104.     except
  105.       on E: Exception do
  106.         ShowMessage('Error recalculating range: ' + E.Message);
  107.     end;
  108.  
  109.     // Restore original workbook options and deferral flag.
  110.     WorkbookSource.Workbook.Options := SavedOptions;
  111.     DeferFormulaCalculation := WasDeferred;
  112.   finally
  113.     EndTime := GetHighResolutionTime;
  114.     Screen.Cursor := OriginalCursor;
  115.     Application.ProcessMessages;
  116.     ShowMessage(Format('Range recalculation completed in %s.', [FormatElapsedTime(EndTime - StartTime)]));
  117.   end;
  118. end;
  119.  
  120.  
  121. procedure TMainForm.AcRecalculateSelectedCellExecute(Sender: TObject);
  122. var
  123.   Selection: TRect;
  124.   OriginalCursor: TCursor;
  125. begin
  126.   OriginalCursor := Screen.Cursor;
  127.   Screen.Cursor := crHourglass;
  128.   Application.ProcessMessages;
  129.   try
  130.     // Create a one-cell selection at the active cell.
  131.     Selection := Rect(WorksheetGrid.Col, WorksheetGrid.Row, WorksheetGrid.Col, WorksheetGrid.Row);
  132.     RecalculateSelectedRange(Selection);
  133.   finally
  134.     Screen.Cursor := OriginalCursor;
  135.     Application.ProcessMessages;
  136.   end;
  137. end;
  138.  
  139. procedure TMainForm.AcRecalculateSelectedRangeExecute(Sender: TObject);
  140. var
  141.   OriginalCursor: TCursor;
  142. begin
  143.   OriginalCursor := Screen.Cursor;
  144.   Screen.Cursor := crHourglass;
  145.   Application.ProcessMessages;
  146.   try
  147.     RecalculateSelectedRange(WorksheetGrid.Selection);
  148.   finally
  149.     Screen.Cursor := OriginalCursor;
  150.     Application.ProcessMessages;
  151.   end;
  152. end;                                        
  153.  

I have added property DeferFormulaCalculation: Boolean read FDeferFormulaCalculation write FDeferFormulaCalculation, which is used to defer whole spreadsheet calculation for specific custom registered formulas which are veeerrryyy long time running functions. So calculation is supposed to be manually triggered by user.

Code: Pascal  [Select][+][-]
  1.   public
  2.     { public declarations }
  3.     property DeferFormulaCalculation: Boolean read FDeferFormulaCalculation write FDeferFormulaCalculation;  //Added by Zlatko Matić    
  4. ...
  5.  
  6.   private
  7.     { private declarations }
  8.     FDeferFormulaCalculation: Boolean;
  9.  
  10. ...
  11. procedure TMainForm.ReadFromIni;  
  12. ...
  13.     //Defer Formula Calculation
  14.     FDeferFormulaCalculation := ini.ReadBool('Settings', 'DeferFormulaCalculation', True);    
  15. ...
  16. //Being used in callback functions for custom registered fpspreadsheet functions:
  17.   // Now check if deferral is active
  18.   if DeferFormulaCalculation then begin
  19.     Result := StringResult('Deferred');
  20.     Exit;
  21.   end;  
  22. ...
  23. //And most  importantly, being used to setup WorkbookSource.Workbook.Options:
  24. procedure TMainForm.AcFileNewExecute(Sender: TObject);
  25. begin
  26.   WorkbookSource.CreateNewWorkbook;
  27.  
  28.   //Formula Calculation Deferring. Added by Zlatko Matić
  29.   if DeferFormulaCalculation then begin
  30.     WorkbookSource.Workbook.Options:=WorkbookSource.Workbook.Options - [boAutoCalc, boCalcBeforeSaving];
  31.   end;  
  32. ...
  33. procedure TMainForm.LoadFile(const AFileName: String);        
  34. ...
  35.     if DeferFormulaCalculation then begin
  36.       WorkbookSource.Workbook.Options:=WorkbookSource.Workbook.Options - [boAutoCalc, boCalcBeforeSaving];
  37.     end;  
  38.  

Btw, please note that, at least in my CodeTyphon distribution, FindFormula is method of Worksheet, not WorkBook (as you said in your example)?

Code: Pascal  [Select][+][-]
  1. sheet := WorkbookSource.Worksheet;  
  2. f := sheet.Formulas.FindFormula(row, col);  
  3.  

« Last Edit: February 13, 2025, 09:10:14 am by tatamata »

wp

  • Hero Member
  • *****
  • Posts: 12696
Re: fpspreadsheet (Spready) - recalculate only selected cell's formula
« Reply #5 on: February 13, 2025, 11:42:42 am »
Btw, please note that, at least in my CodeTyphon distribution, FindFormula is method of Worksheet, not WorkBook (as you said in your example)?
A typo...

Since you are mentioning CodeTyphon here, I took a look into their recent 8.60 release to see what they made out of my files. The modifications in the unit headers are much more decent now compared to a few years ago. However, there are still tendencies to name things as if they were theirs: In the comment to the function WrapText in unit fpsvisualutils, they have
Quote
  @note    Based on ocde posted by user "taazz" in the Typhon forum
           http://forum.lazarus.freepascal.org/index.php/topic,21305.msg124743.html#msg124743
which changed my "Lazarus forum" to "Typhon forum". Ridiculous!

tatamata

  • Hero Member
  • *****
  • Posts: 804
    • ZMSQL - SQL enhanced in-memory database
Re: fpspreadsheet (Spready) - recalculate only selected cell's formula
« Reply #6 on: February 13, 2025, 12:17:03 pm »
Ah, I would rather not participate in that discussion CodeTyphon vs Lazarus  :)
I took my decision long time ago to use CodeTyphon rather than stock Lazarus, primarily because of convenience - it is so much easier to install and upgrade, it is much more robust and convenient in my opinion. Especially when it comes to multiplatform compilation. It just works.
On other hand, I consider CodeTyphon just being an alternative Lazarus distribution...a very good one, in my opinion. In my mind there is no separation between Lazarus and CodeTyphon, it is one thing.
I am also a person they took my component into CodeTyphon, the ZMSQL package. Not only that I agree and adorse, actually I am very happy they took it and embedded into CodeTyphon. Without that, my component would probably fall into obscurity. For that reason, long live CodeTyphon!  :D
Anyway, thank you a lot for help, with your help I was able to resolve this deferring and explicit formula calculation on demand, and this was really critical for me!
« Last Edit: February 13, 2025, 12:21:52 pm by tatamata »

 

TinyPortal © 2005-2018