From 0ab8ce90cb1dd9d7b1c9633994845ec00a19fc5f Mon Sep 17 00:00:00 2001
From: Jaap Jansma <jaap.jansma@civicoop.org>
Date: Wed, 24 Apr 2024 13:49:09 +0200
Subject: [PATCH] added bounded calculation

---
 Civi/DataProcessor/Factory.php                |   1 +
 ...dedAggregateFunctionFieldOutputHandler.php | 141 ++++++++++++++++++
 ...dedAggregateFunctionFieldOutputHandler.tpl |  61 ++++++++
 3 files changed, 203 insertions(+)
 create mode 100644 Civi/DataProcessor/FieldOutputHandler/BoundedAggregateFunctionFieldOutputHandler.php
 create mode 100644 templates/CRM/Dataprocessor/Form/Field/Configuration/BoundedAggregateFunctionFieldOutputHandler.tpl

diff --git a/Civi/DataProcessor/Factory.php b/Civi/DataProcessor/Factory.php
index 8a6aa276..9b9f650c 100644
--- a/Civi/DataProcessor/Factory.php
+++ b/Civi/DataProcessor/Factory.php
@@ -227,6 +227,7 @@ class Factory {
     $this->addOutputHandler('calculations_total', new Definition('Civi\DataProcessor\FieldOutputHandler\Calculations\TotalFieldOutputHandler'), E::ts('Calculations (on multiple fields): Adding up'));
     $this->addOutputHandler('calculations_percentage_change', new Definition('Civi\DataProcessor\FieldOutputHandler\Calculations\PercentageChangeFieldOutputHandler'), E::ts('Calculations (on multiple fields): Percentage Change'));
     $this->addOutputHandler('aggregation_function', new Definition('Civi\DataProcessor\FieldOutputHandler\AggregateFunctionFieldOutputHandler'), E::ts('Aggregation function'));
+    $this->addOutputHandler('bounded_aggregation_function', new Definition('Civi\DataProcessor\FieldOutputHandler\BoundedAggregateFunctionFieldOutputHandler'), E::ts('Bounded Aggregation function'));
     $this->addOutputHandler('image', new Definition('Civi\DataProcessor\FieldOutputHandler\ImageFieldOutputHandler'), E::ts('Image'));
     $this->addOutputHandler('array', new Definition('Civi\DataProcessor\FieldOutputHandler\ArrayFieldOutputHandler'), E::ts('Multivalue Field as Array'));
     $this->addOutputHandler('sanitize_string', new Definition('Civi\DataProcessor\FieldOutputHandler\SanitizeString'), E::ts('Replaces special characters by their HTML code and newlines by \\n.'));
diff --git a/Civi/DataProcessor/FieldOutputHandler/BoundedAggregateFunctionFieldOutputHandler.php b/Civi/DataProcessor/FieldOutputHandler/BoundedAggregateFunctionFieldOutputHandler.php
new file mode 100644
index 00000000..93a44610
--- /dev/null
+++ b/Civi/DataProcessor/FieldOutputHandler/BoundedAggregateFunctionFieldOutputHandler.php
@@ -0,0 +1,141 @@
+<?php
+/**
+ * Copyright (C) 2024  Jaap Jansma (jaap.jansma@civicoop.org)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+namespace Civi\DataProcessor\FieldOutputHandler;
+
+use CRM_Dataprocessor_ExtensionUtil as E;
+
+class BoundedAggregateFunctionFieldOutputHandler extends AggregateFunctionFieldOutputHandler {
+
+  /**
+   * @var float
+   */
+  protected $minBound = null;
+
+  /**
+   * @var float
+   */
+  protected $minValue = 0.00;
+
+  /**
+   * @var float
+   */
+  protected $maxBound = null;
+
+  /**
+   * @var float
+   */
+  protected $maxValue = 0.00;
+
+  /**
+   * Initialize the processor
+   *
+   * @param String $alias
+   * @param String $title
+   * @param array $configuration
+   * @param \Civi\DataProcessor\ProcessorType\AbstractProcessorType $processorType
+   */
+  public function initialize($alias, $title, $configuration) {
+    parent::initialize($alias, $title, $configuration);
+    if (isset($configuration['min_bound']) && strlen($configuration['min_bound'])) {
+      $this->minBound = (float) $configuration['min_bound'];
+      $this->minValue = (float) $configuration['min_value'];
+    }
+    if (isset($configuration['max_bound']) && strlen($configuration['max_bound'])) {
+      $this->maxBound = (float) $configuration['max_bound'];
+      $this->maxValue = (float) $configuration['max_value'];
+    }
+  }
+
+  /**
+   * Returns the formatted value
+   *
+   * @param $rawRecord
+   * @param $formattedRecord
+   *
+   * @return \Civi\DataProcessor\FieldOutputHandler\FieldOutput
+   */
+  public function formatField($rawRecord, $formattedRecord) {
+    $value = (float) $rawRecord[$this->aggregateField->alias];
+    if ($this->minBound !== null && $value < $this->minBound) {
+      $value = $this->minValue;
+    } elseif ($this->maxBound !== null && $value > $this->maxBound) {
+      $value = $this->maxValue;
+    } elseif ($this->roundToNearestWholeNumber) {
+      $value = $this->roundUpToNearest($value, $this->roundToNearestWholeNumber);
+    }
+    return $this->formatOutput($value);
+  }
+
+  /**
+   * When this handler has additional configuration you can add
+   * the fields on the form with this function.
+   *
+   * @param \CRM_Core_Form $form
+   * @param array $field
+   */
+  public function buildConfigurationForm(\CRM_Core_Form $form, $field=array()) {
+    parent::buildConfigurationForm($form, $field);
+    $form->add('number', 'min_bound', E::ts('Bound Value (if value is less then)'), ['class' => 'six', 'min' => 1], true);
+    $form->add('number', 'min_value', E::ts('Min Value'), ['class' => 'six', 'min' => 1], true);
+    $form->add('number', 'max_bound', E::ts('Bound Value (if value is more than)'), ['class' => 'six', 'min' => 1], true);
+    $form->add('number', 'max_value', E::ts('Max Value'), ['class' => 'six', 'min' => 1], true);
+    if (isset($field['configuration'])) {
+      $configuration = $field['configuration'];
+      if (isset($configuration['min_bound'])) {
+        $this->defaults['min_bound'] = $configuration['min_bound'];
+      }
+      if (isset($configuration['min_value'])) {
+        $this->defaults['min_value'] = $configuration['min_value'];
+      }
+      if (isset($configuration['max_bound'])) {
+        $this->defaults['max_bound'] = $configuration['max_bound'];
+      }
+      if (isset($configuration['max_value'])) {
+        $this->defaults['max_value'] = $configuration['max_value'];
+      }
+      $form->setDefaults($this->defaults);
+    }
+  }
+
+  /**
+   * When this handler has configuration specify the template file name
+   * for the configuration form.
+   *
+   * @return false|string
+   */
+  public function getConfigurationTemplateFileName() {
+    return "CRM/Dataprocessor/Form/Field/Configuration/BoundedAggregateFunctionFieldOutputHandler.tpl";
+  }
+
+  /**
+   * Process the submitted values and create a configuration array
+   *
+   * @param $submittedValues
+   * @return array
+   */
+  public function processConfiguration($submittedValues) {
+    $configuration = parent::processConfiguration($submittedValues);
+    $configuration['min_bound'] = $submittedValues['min_bound'];
+    $configuration['min_value'] = $submittedValues['min_value'];
+    $configuration['max_bound'] = $submittedValues['max_bound'];
+    $configuration['max_value'] = $submittedValues['max_value'];
+    return $configuration;
+  }
+
+}
diff --git a/templates/CRM/Dataprocessor/Form/Field/Configuration/BoundedAggregateFunctionFieldOutputHandler.tpl b/templates/CRM/Dataprocessor/Form/Field/Configuration/BoundedAggregateFunctionFieldOutputHandler.tpl
new file mode 100644
index 00000000..8d32df16
--- /dev/null
+++ b/templates/CRM/Dataprocessor/Form/Field/Configuration/BoundedAggregateFunctionFieldOutputHandler.tpl
@@ -0,0 +1,61 @@
+{crmScope extensionKey='dataprocessor'}
+  <div class="crm-section">
+    <div class="label">{$form.function.label}</div>
+    <div class="content">{$form.function.html}</div>
+    <div class="clear"></div>
+  </div>
+  <div class="crm-section">
+      <div class="label">{$form.field.label}</div>
+      <div class="content">{$form.field.html}</div>
+      <div class="clear"></div>
+  </div>
+
+  <div class="crm-section">
+    <div class="label">{$form.number_of_decimals.label}</div>
+    <div class="content">{$form.number_of_decimals.html}
+      <p class="description">{ts}Leave empty for no formatting{/ts}</p>
+    </div>
+    <div class="clear"></div>
+  </div>
+  <div class="crm-section">
+    <div class="label">{$form.decimal_separator.label}</div>
+    <div class="content">{$form.decimal_separator.html}</div>
+    <div class="clear"></div>
+  </div>
+  <div class="crm-section">
+    <div class="label">{$form.thousand_separator.label}</div>
+    <div class="content">{$form.thousand_separator.html}</div>
+    <div class="clear"></div>
+  </div>
+  <div class="crm-section">
+    <div class="label">{$form.prefix.label}</div>
+    <div class="content">{$form.prefix.html}</div>
+    <div class="clear"></div>
+  </div>
+  <div class="crm-section">
+    <div class="label">{$form.suffix.label}</div>
+    <div class="content">{$form.suffix.html}</div>
+    <div class="clear"></div>
+  </div>
+  <div class="crm-section">
+    <div class="label">{$form.min_bound.label}</div>
+    <div class="content">{$form.min_bound.html} {$form.min_value.html}</div>
+    <div class="clear"></div>
+  </div>
+  <div class="crm-section">
+    <div class="label">{$form.max_bound.label}</div>
+    <div class="content">{$form.max_bound.html} {$form.max_value.html}</div>
+    <div class="clear"></div>
+  </div>
+  <div class="crm-section">
+    <div class="label">{$form.roundToNearestWholeNumber.label}</div>
+    <div class="content">{$form.roundToNearestWholeNumber.html}</div>
+    <div class="clear"></div>
+  </div>
+  <div class="crm-section">
+    <div class="label">{$form.multiplier.label}</div>
+    <div class="content">{$form.multiplier.html}</div>
+    <div class="clear"></div>
+  </div>
+
+{/crmScope}
-- 
GitLab