sigd/app/Services/Activity/FormMitigasiService.php

889 lines
40 KiB
PHP

<?php
namespace App\Services\Activity;
use App\Models\KegiatanMitigasi;
use App\Models\EmissionFactorReference;
use App\Repositories\ActivityDataRepository;
use App\Repositories\EmissionFactorRepository;
use App\Repositories\EmissionReductionRepository;
use App\Helpers\FormulaEvaluator;
use Illuminate\Support\Facades\DB;
class FormMitigasiService
{
protected array $unitCategoryMap = [
'liter' => 'volume',
'kiloliter' => 'volume',
'm3' => 'volume',
'mmbtu' => 'energy',
'mwh' => 'energy',
'boe' => 'energy',
'ton' => 'mass',
'kilogram' => 'mass',
'giga gram' => 'mass',
];
protected array $unitConversionFactor = [
'liter' => 0.000000001,
'kiloliter' => 0.001,
'm3' => 0.001,
'mmbtu' => 0.00105587,
'mwh' => 0.0036,
'boe' => 0.0058,
'ton' => 0.001,
'kilogram' => 0.000001,
'giga gram' => 1.0,
];
public function __construct(
protected ActivityDataRepository $adRepo,
protected EmissionFactorRepository $efRepo,
protected EmissionReductionRepository $erRepo,
protected FormulaEvaluator $evaluator
) {}
protected function cast($v)
{
if (is_null($v)) return 0;
if (is_numeric($v)) return $v + 0;
if (is_string($v) && is_numeric(trim($v))) return trim($v) + 0;
return $v;
}
protected function selectReduction($reductions, $adValues, $typeEmissionReduction = null)
{
if (!$reductions) return null;
if (is_array($reductions)) {
$array = $reductions;
} elseif (method_exists($reductions, 'all')) {
$array = $reductions->all();
} else {
$array = (array)$reductions;
}
if (count($array) === 0) return null;
if (count($array) === 1) return $array[0];
if (!empty($adValues['AD1'])) {
$match = strtolower(trim($adValues['AD1']));
foreach ($array as $reduction) {
if($reduction['type_emission_reduction'] == 1 && $match == strtolower("PJU sebelumnya terkoneksi dengan lgrid")) {
return $reduction;
} else if ($reduction['type_emission_reduction'] == 2 && $match == strtolower("PJU sebelumnya terkoneksi dengan PLTD")) {
return $reduction;
}
}
}
if ($typeEmissionReduction !== null) {
foreach ($array as $reduction) {
$typeEr = is_array($reduction)
? ($reduction['type_emission_reduction'] ?? null)
: ($reduction->type_emission_reduction ?? null);
if ((int)$typeEr === (int)$typeEmissionReduction) {
return $reduction;
}
}
}
return $array[0];
}
public function calculateAndPersist(string $mitigationId, KegiatanMitigasi $km): void
{
DB::transaction(function() use ($mitigationId, $km) {
logger()->info('--- calculateAndPersist start ---');
$adForms = $this->adRepo->getByMitigation($km->id);
$adValues = [];
$adArrays = [];
$adUnits = [];
$labelMap = [];
$typeEmissionReduction = null;
foreach ($adForms as $item) {
$key = 'AD' . $item->sequence;
$rawArray = $item->values_array ?: [];
if (!is_array($rawArray)) $rawArray = [];
if (empty($rawArray) || (count($rawArray) == 1 && $rawArray[0] === null)) {
$rawArray = [$item->value];
}
$values = array_map(fn($x) => $this->cast($x), $rawArray);
if (empty($values)) $values = [0];
$adArrays[$key] = $values;
$adValues[$key] = (is_array($values) && count($values) > 0) ? end($values) : 0;
$adUnits[$key] = strtolower($item->unit ?? '');
if (property_exists($item, 'label') && $item->label) {
$labelMap[$item->label] = $key;
}
if (!$typeEmissionReduction && !empty($item->type_emission_reduction)) {
$typeEmissionReduction = $item->type_emission_reduction;
}
}
logger()->info('ADValues', $adValues);
logger()->info('ADArrays', $adArrays);
logger()->info('ADUnits', $adUnits);
logger()->info('LabelMap', $labelMap);
logger()->info('TypeEmissionReduction', ['value' => $typeEmissionReduction]);
// Build REF_xxx mapping
$contextMaps = DB::table('activity.emission_reduction_context_map')
->where('mitigation_id', $mitigationId)
->get();
$refParams = [];
$refContext = [];
foreach ($contextMaps as $map) {
$adKey = 'AD'.$map->ad_sequence;
$val = $adValues[$adKey] ?? null;
$refParams[$map->ref_key] = [
'param' => $map->param,
'unit' => $map->unit,
'context' => [ $map->context_field => $adKey ],
];
if (!isset($refContext[$map->ref_key][$map->context_field]) || $refContext[$map->ref_key][$map->context_field] === null) {
if ($val !== null) {
$refContext[$map->ref_key][$map->context_field] = $val;
}
}
logger()->info('refContext build', [
'ref_key' => $map->ref_key,
'context_field' => $map->context_field,
'ad_seq' => $map->ad_sequence,
'value' => $val
]);
}
logger()->info('refParams', $refParams);
logger()->info('refContext', $refContext);
$reductions = $this->erRepo->findAllByMitigation($mitigationId);
if (is_object($reductions) && method_exists($reductions, 'toArray')) {
logger()->info('DEBUG: REDUCTIONS', $reductions->toArray());
} else {
logger()->info('DEBUG: REDUCTIONS', (array) $reductions);
}
logger()->info('REDUCTIONS RAW TYPE', ['type' => gettype($reductions), 'class' => is_object($reductions) ? get_class($reductions) : null]);
// --- PEMILIHAN REDUCTION PAKE LOGIC BARU ---
$chosenReduction = $this->selectReduction($reductions, $adValues, $typeEmissionReduction);
if (!$chosenReduction) {
logger()->error('REDUCTIONS EMPTY', [
'reductions' => is_object($reductions) ? $reductions->toArray() : $reductions
]);
throw new \Exception('Emission reduction formula not found.');
}
$formula = ltrim(is_array($chosenReduction) ? $chosenReduction['emission_reduction'] : $chosenReduction->emission_reduction, '=');
logger()->info('FORMULA RAW', ['formula' => $formula]);
logger()->info('FORMULA AFTER REF_CF', ['formula' => $formula]);
$result = $this->evaluateEmissionReductionFormula(
$formula,
$adValues,
$refParams,
$refContext,
$adArrays,
$adUnits,
$labelMap
);
logger()->info('HASIL PERHITUNGAN', ['emission_factor' => $result]);
$km->update(['emission_factor' => $result]);
logger()->info('--- calculateAndPersist DONE ---');
});
}
protected function getRefArray($param, $unit, $context, $adArrays, $adKeySigma)
{
$arr = [];
$adSource = $adArrays[$adKeySigma] ?? [];
foreach ($adSource as $i => $v) {
$q = \App\Models\EmissionFactorReference::where('param', $param)
->where('unit', $unit);
foreach ($context as $field => $adKey) {
$q->where('keterangan', $v);
}
$arr[] = $q->value('value') ?? 0;
}
return $arr;
}
/**
* Parse Σi=ADx*ADy menjadi sum produk per baris.
* Contoh: Σi=AD1*AD2 → (AD1[0]*AD2[0]) + (AD1[1]*AD2[1]) + ...
* @param string $formula
* @param array $adArrays
* @param array $adValues
* @param string $debugTag
* @return string
*/
public function parseSigmaSumProduct($formula, $adArrays, $adValues, $refParams, $refContext, $debugTag = '')
{
logger()->info("Masuk $debugTag", ['formula' => $formula]);
return preg_replace_callback(
'/Σi=(([A-Za-z0-9_\.]+|\Σi=[A-Za-z0-9_\.]+)(\s*[\*\/]\s*([A-Za-z0-9_\.]+|\Σi=[A-Za-z0-9_\.]+))+)/u',
function ($m) use ($adArrays, $adValues, $refParams, $refContext) {
$raw = $m[1];
$parts = preg_split('/\s*[\*\/]\s*/', $raw);
preg_match_all('/[\*\/]/', $raw, $opsRaw);
$ops = $opsRaw[0];
$arrays = [];
$maxRows = 1;
foreach ($parts as $p) {
$p = preg_replace('/^Σi=/', '', $p);
// AD array
if (isset($adArrays[$p]) && is_array($adArrays[$p]) && count($adArrays[$p]) > 1) {
$arr = array_map('floatval', $adArrays[$p]);
}
// REF array (array dari replaceREF as_array true)
else if (preg_match('/^REF_([A-Z0-9_]+)$/i', $p, $mm)) {
$refKey = $mm[1];
$arr = [0];
if (isset($refParams[$refKey])) {
$rawRef = $this->replaceREF("REF_$refKey", $refParams, $refContext, $adArrays, $adValues, [], '', true);
if (is_string($rawRef)) $rawRef = explode(',', $rawRef);
if (!is_array($rawRef)) $rawRef = [$rawRef];
$arr = array_map('floatval', $rawRef);
if (!$arr || count($arr) === 0) $arr = [0];
}
}
// Scalar (AD/REF satuan, angka, konstanta)
else if (isset($adArrays[$p]) && count($adArrays[$p]) === 1) {
$arr = [floatval($adArrays[$p][0])];
} else if (isset($adValues[$p])) {
$arr = [floatval($adValues[$p])];
} else if (is_numeric($p)) {
$arr = [floatval($p)];
} else {
$arr = [0];
}
$arrays[] = $arr;
$maxRows = max($maxRows, count($arr));
}
// Expand scalar ke semua baris jika perlu
foreach ($arrays as $i => $arr) {
if (count($arr) < $maxRows) {
$scalar = isset($arr[0]) ? $arr[0] : 0;
$arrays[$i] = array_fill(0, $maxRows, $scalar);
}
}
// SUMPRODUCT manual per baris
$sum = 0;
for ($i = 0; $i < $maxRows; $i++) {
$val = $arrays[0][$i];
for ($j = 1; $j < count($arrays); $j++) {
$op = $ops[$j - 1] ?? '*';
$v = $arrays[$j][$i];
if ($op == '*') $val *= $v;
else $val /= ($v != 0 ? $v : 1);
}
$sum += $val;
}
return (string)$sum;
},
$formula
);
}
/**
* Ganti semua blok SUMPRODUCT( … ) dengan hasil angka (Excel-like)
* - Support inner expression rumit, nested, pow, persen, kurung
* - Support AD/REF array, scalar, mixed, otomatis looping max row
* - Support nested SUMPRODUCT di dalam inner (rekursif)
* - Tidak support Σi=, Σj= (sigma): Khusus SUMPRODUCT saja
* @param string $formula
* @param array $adArrays
* @param array $adValues
* @param array $refParams
* @param array $refContext
* @return string
*/
public function parseSumproductExcel($formula, $adArrays, $adValues, $refParams, $refContext)
{
// Selama masih ada SUMPRODUCT(
while (preg_match('/SUMPRODUCT\s*\(/i', $formula)) {
$formula = preg_replace_callback(
'/SUMPRODUCT\s*\(([^()]*(?:\((?:[^()]++|(?1))*\)[^()]*)*)\)/i',
function ($m) use ($adArrays, $adValues, $refParams, $refContext) {
$inner = $m[1];
// 1. Cari semua variabel array
preg_match_all('/\b(AD[0-9]+|REF_[A-Z0-9_]+)\b/', $inner, $matches);
$vars = array_unique($matches[1]);
// 2. Dapatkan max rows (array terpanjang)
$maxRows = 1;
foreach ($vars as $v) {
if (isset($adArrays[$v]) && is_array($adArrays[$v]))
$maxRows = max($maxRows, count($adArrays[$v]));
}
$sum = 0;
for ($i = 0; $i < $maxRows; $i++) {
// Ganti AD
$expr = preg_replace_callback('/AD([0-9]+)/', function($mm) use ($adArrays, $adValues, $i) {
$adKey = "AD".$mm[1];
return isset($adArrays[$adKey][$i]) ? $adArrays[$adKey][$i] :
(isset($adArrays[$adKey][0]) ? $adArrays[$adKey][0] : (isset($adValues[$adKey]) ? $adValues[$adKey] : 0));
}, $inner);
// Ganti REF
$expr = preg_replace_callback('/REF_([A-Z0-9_]+)/', function($mm) use ($refParams, $refContext, $adArrays, $adValues, $i) {
$key = $mm[1];
if (!isset($refParams[$key])) return 0;
$map = $refParams[$key];
$contextKey = null;
foreach ($map['context'] as $field => $adKeyC) {
$contextKey = $adKeyC;
}
$adVal = $contextKey && isset($adArrays[$contextKey][$i]) ? $adArrays[$contextKey][$i] : ($adValues[$contextKey] ?? null);
$q = \App\Models\EmissionFactorReference::where('param', $map['param'])
->where('unit', $map['unit'])
->where('keterangan', $adVal);
$val = $q->value('value');
return $val ?? 0;
}, $expr);
// Ganti persen & pow
$expr = preg_replace_callback('/(\d+(?:\.\d+)?)%/', fn($mm) => $mm[1]/100, $expr);
$expr = preg_replace('/([0-9.Ee+\-]+)\s*\^\s*([0-9.Ee+\-]+)/', 'pow($1,$2)', $expr);
// Eval tiap baris, tambahkan ke sum
$expr = str_replace('CONST_', '', $expr);
// Special case
if (strpos($expr, 'SUM(') !== false) {
foreach (['AD3', 'AD4'] as $adKey) {
if (strpos($expr, "SUM($adKey)") !== false && isset($adArrays[$adKey])) {
$expr = str_replace("SUM($adKey)", array_sum($adArrays[$adKey]), $expr);
}
}
// Tambah di sini:
$expr = preg_replace('/SUM\(\s*([0-9\.]+)\s*\)/', '$1', $expr);
// Opsional: hapus SUM lain
$expr = str_replace('SUM', '', $expr);
}
try {
$sum += eval('return ' . $expr . ';');
} catch (\Throwable $e) {
logger()->error("[SUMPRODUCT] Eval row $i error: $expr | " . $e->getMessage());
}
}
return (string)$sum;
},
$formula
);
}
return $formula;
}
public function isSigmaArrayFormula($formula, $adArrays, $refParams) {
if (strpos($formula, 'Σi=') === false) return false;
// Ambil semua yang dalam Σi=(....)
if (preg_match('/Σi=\(([^)]+)\)/', $formula, $sumproductPart)) {
// Semua variabel AD/REF/CONST
preg_match_all('/\b(AD[0-9]+|REF_[A-Z0-9_]+)\b/', $sumproductPart[1], $vars);
foreach ($vars[1] as $var) {
if (isset($adArrays[$var]) && is_array($adArrays[$var]) && count($adArrays[$var]) > 1) return true;
}
}
return false;
}
/**
* Ganti Σi=ADxx dengan hasil sum seluruh array ADxx.
* Contoh: Σi=AD1 → 19 (jika [10,5,4])
*/
public function parseSigmaSumOnly($formula, $adArrays, $adValues, $refParams = [], $refContext = [], $debugTag = '')
{
logger()->info("Masuk $debugTag", ['formula' => $formula]);
$self = $this; // Closure binding
// Σi=ADxx atau Σi=REF_xxx
$formula = preg_replace_callback('/[Σ∑]i\s*=?\s*([A-Za-z0-9_]+)/u', function ($m) use ($adArrays, $adValues, $refParams, $refContext, $self) {
$key = $m[1];
// Handle AD array
if (isset($adArrays[$key])) {
$arr = $adArrays[$key];
if (count($arr) === 0 && isset($adValues[$key])) $arr = [$adValues[$key]];
$sum = array_sum(array_filter($arr, 'is_numeric'));
logger()->info("parseSigmaSumOnly: SUM for $key", ['array' => $arr, 'sum' => $sum]);
return $sum;
}
// Handle REF_xxx (ambil array REF, lalu sum)
elseif (preg_match('/^REF_([A-Z0-9_]+)$/i', $key, $mm)) {
$refKey = $mm[1];
// Pakai replaceREF tapi as_array=true!
$refArr = $self->replaceREF(
"REF_$refKey",
$refParams,
$refContext,
$adArrays,
$adValues,
[], // adUnits
'parseSigmaSumOnly_REF',
true // AS ARRAY
);
if (is_string($refArr)) {
$refArr = explode(',', $refArr);
}
if (!is_array($refArr)) $refArr = [$refArr];
$arr = array_map('floatval', $refArr);
if (!$arr || count($arr) === 0) $arr = [0];
$sum = array_sum(array_filter($refArr, 'is_numeric'));
logger()->info("parseSigmaSumOnly: SUM for REF $key", ['array' => $refArr, 'sum' => $sum]);
return $sum;
}
// Scalar fallback
elseif (isset($adValues[$key]) && is_numeric($adValues[$key])) {
return $adValues[$key];
}
return 0;
}, $formula);
return $formula;
}
/**
* Evaluate the emission reduction formula with given parameters.
*/
// public function evaluateEmissionReductionFormula($formula, $adValues, $refParams, $refContext, $adArrays, $adUnits = [], $labelMap = []) {
// logger()->info("evaluateEmissionReductionFormula: START", ['formula' => $formula]);
// if ($this->isSigmaArrayFormula($formula, $adArrays, $refParams)) {
// // Hitung maxRows DARI variabel dalam Σi
// preg_match('/Σi=\(([^)]+)\)/', $formula, $m);
// preg_match_all('/\b(AD[0-9]+|REF_[A-Z0-9_]+)\b/', $m[1] ?? '', $vars);
// $maxRows = 1;
// foreach ($vars[1] as $var) {
// if (isset($adArrays[$var]) && is_array($adArrays[$var]))
// $maxRows = max($maxRows, count($adArrays[$var]));
// }
// $resultRows = [];
// for ($i = 0; $i < $maxRows; $i++) {
// // Build per baris
// $adValuesRow = [];
// $adArraysRow = [];
// foreach ($adArrays as $key => $arr) {
// $adArraysRow[$key] = [isset($arr[$i]) ? $arr[$i] : (isset($arr[0]) ? $arr[0] : 0)];
// $adValuesRow[$key] = isset($arr[$i]) ? $arr[$i] : (isset($arr[0]) ? $arr[0] : 0);
// }
// $f = $this->parseSigmaSumProduct($formula, $adArraysRow, $adValuesRow, $refParams, $refContext, "row-$i-parseSigmaSumProduct");
// $f = $this->parseSigmaSumOnly($f, $adArraysRow, $adValuesRow, $refParams, $refContext, "row-$i-parseSigmaSumOnly");
// $f = $this->replaceADDefault($f, $adArraysRow, $adValuesRow, "row-$i-replaceADDefault");
// $f = $this->replaceREF($f, $refParams, $refContext, $adArraysRow, $adValuesRow, $adUnits, "row-$i-replaceREF", false);
// $f = preg_replace_callback('/(\d+(?:\.\d+)?)%/', fn($mm) => $mm[1] / 100, $f);
// $f = preg_replace('/([A-Za-z0-9_\.]+)\^([0-9]+)/', 'pow($1,$2)', $f);
// // Clean up kurung, biar ga unmatched
// $f = preg_replace('/\)\)+$/', ')', $f);
// $open = substr_count($f, '('); $close = substr_count($f, ')');
// if ($open > $close) $f .= str_repeat(')', $open - $close);
// if ($close > $open) $f = str_repeat('(', $close - $open) . $f;
// // Remove sigma
// $f = preg_replace('/^(b")?([Σ∑Î]i=)/u', '', $f);
// logger()->info("BARIS $i - Formula Final Eval", ['formula' => $f]);
// try {
// $res = eval('return ' . $f . ';');
// $resultRows[] = $res;
// } catch (\Throwable $e) {
// logger()->error("Eval error BARIS $i: $f | " . $e->getMessage());
// $resultRows[] = 0;
// }
// }
// $result = array_sum($resultRows);
// logger()->info("evaluateEmissionReductionFormula: RESULT TOTAL", ['result' => $result, 'rows' => $resultRows]);
// return $result;
// }
// // --- Pipeline normal ---
// $formula = $this->parseSigmaSumProduct($formula, $adArrays, $adValues, $refParams, $refContext, 'parseSigmaSumProduct');
// $formula = $this->parseSigmaSumOnly($formula, $adArrays, $adValues, $refParams, $refContext, 'parseSigmaSumOnly');
// $formula = $this->replaceADDefault($formula, $adArrays, $adValues, 'replaceADDefault');
// $formula = $this->replaceREF($formula, $refParams, $refContext, $adArrays, $adValues, $adUnits, 'replaceREF', false);
// $formula = preg_replace_callback('/(\d+(?:\.\d+)?)%/', fn($mm) => $mm[1] / 100, $formula);
// $formula = preg_replace('/([A-Za-z0-9_\.]+)\^([0-9]+)/', 'pow($1,$2)', $formula);
// $open = substr_count($formula, '('); $close = substr_count($formula, ')');
// if ($open > $close) $formula .= str_repeat(')', $open - $close);
// if ($close > $open) $formula = str_repeat('(', $close - $open) . $formula;
// logger()->info("evaluateEmissionReductionFormula: FINAL FORMULA", ['formula' => $formula]);
// try {
// $result = eval('return ' . $formula . ';');
// logger()->info("evaluateEmissionReductionFormula: RESULT", ['result' => $result]);
// return $result;
// } catch (\Throwable $e) {
// logger()->error("Eval error FINAL: $formula | " . $e->getMessage());
// return 0;
// }
// }
public function evaluateEmissionReductionFormula($formula, $adValues, $refParams, $refContext, $adArrays, $adUnits = [], $labelMap = [])
{
logger()->info("evaluateEmissionReductionFormula: START", ['formula' => $formula]);
// $formula = $this->parseSumproductExcelLikeV3($formula, $adArrays, $adValues, $refParams, $refContext);
// 1. PARSE SEMUA BLOK SUMPRODUCT(…) (Excel style)
$formula = $this->parseSumproductExcel($formula, $adArrays, $adValues, $refParams, $refContext);
// 2. Sigma / Σi= pipeline (legacy, kalau ada)
if ($this->isSigmaArrayFormula($formula, $adArrays, $refParams)) {
preg_match('/Σi=\(([^)]+)\)/', $formula, $m);
preg_match_all('/\b(AD[0-9]+|REF_[A-Z0-9_]+)\b/', $m[1] ?? '', $vars);
$maxRows = 1;
foreach ($vars[1] as $var) {
if (isset($adArrays[$var]) && is_array($adArrays[$var]))
$maxRows = max($maxRows, count($adArrays[$var]));
}
$resultRows = [];
for ($i = 0; $i < $maxRows; $i++) {
// Build per baris
$adValuesRow = [];
$adArraysRow = [];
foreach ($adArrays as $key => $arr) {
$adArraysRow[$key] = [isset($arr[$i]) ? $arr[$i] : (isset($arr[0]) ? $arr[0] : 0)];
$adValuesRow[$key] = isset($arr[$i]) ? $arr[$i] : (isset($arr[0]) ? $arr[0] : 0);
}
$f = $this->parseSigmaSumProduct($formula, $adArraysRow, $adValuesRow, $refParams, $refContext, "row-$i-parseSigmaSumProduct");
$f = $this->parseSigmaSumOnly($f, $adArraysRow, $adValuesRow, $refParams, $refContext, "row-$i-parseSigmaSumOnly");
$f = $this->replaceADDefault($f, $adArraysRow, $adValuesRow, "row-$i-replaceADDefault");
$f = $this->replaceREF($f, $refParams, $refContext, $adArraysRow, $adValuesRow, $adUnits, "row-$i-replaceREF", false);
$f = preg_replace_callback('/(\d+(?:\.\d+)?)%/', fn($mm) => $mm[1] / 100, $f);
$f = preg_replace('/([A-Za-z0-9_\.]+)\^([0-9]+)/', 'pow($1,$2)', $f);
// Clean up kurung
$f = preg_replace('/\)\)+$/', ')', $f);
$open = substr_count($f, '('); $close = substr_count($f, ')');
if ($open > $close) $f .= str_repeat(')', $open - $close);
if ($close > $open) $f = str_repeat('(', $close - $open) . $f;
$f = preg_replace('/^(b")?([Σ∑Î]i=)/u', '', $f);
$formula = str_replace('CONST_', '', $formula);
$formula = preg_replace('/^(b")?([Σ∑Î][ijk]=)/u', '', $formula);
logger()->info("BARIS $i - Formula Final Eval", ['formula' => $f]);
try {
$res = eval('return ' . $f . ';');
$resultRows[] = $res;
} catch (\Throwable $e) {
logger()->error("Eval error BARIS $i: $f | " . $e->getMessage());
$resultRows[] = 0;
}
}
$result = array_sum($resultRows);
logger()->info("evaluateEmissionReductionFormula: RESULT TOTAL", ['result' => $result, 'rows' => $resultRows]);
return $result;
}
// 3. Pipeline normal non-sigma
$formula = $this->parseSigmaSumProduct($formula, $adArrays, $adValues, $refParams, $refContext, 'parseSigmaSumProduct');
$formula = $this->parseSigmaSumOnly($formula, $adArrays, $adValues, $refParams, $refContext, 'parseSigmaSumOnly');
$formula = $this->replaceADDefault($formula, $adArrays, $adValues, 'replaceADDefault');
$formula = $this->replaceREF($formula, $refParams, $refContext, $adArrays, $adValues, $adUnits, 'replaceREF', false);
$formula = preg_replace_callback('/(\d+(?:\.\d+)?)%/', fn($mm) => $mm[1] / 100, $formula);
$formula = preg_replace('/([A-Za-z0-9_\.]+)\^([0-9]+)/', 'pow($1,$2)', $formula);
// Balance kurung
$open = substr_count($formula, '('); $close = substr_count($formula, ')');
if ($open > $close) $formula .= str_repeat(')', $open - $close);
if ($close > $open) $formula = str_repeat('(', $close - $open) . $formula;
// Special case
if (strpos($formula, 'SUM(') !== false) {
foreach (['AD3', 'AD4'] as $adKey) {
if (strpos($formula, "SUM($adKey)") !== false && isset($adArrays[$adKey])) {
$formula = str_replace("SUM($adKey)", array_sum($adArrays[$adKey]), $formula);
}
}
// Tambah di sini:
$formula = preg_replace('/SUM\(\s*([0-9\.]+)\s*\)/', '$1', $formula);
// Opsional: hapus SUM lain
$formula = str_replace('SUM', '', $formula);
}
logger()->info("evaluateEmissionReductionFormula: FINAL FORMULA", ['formula' => $formula]);
try {
$result = eval('return ' . $formula . ';');
logger()->info("evaluateEmissionReductionFormula: RESULT", ['result' => $result]);
return $result;
} catch (\Throwable $e) {
logger()->error("Eval error FINAL: $formula | " . $e->getMessage());
return 0;
}
}
function replaceADDefault($formula, $adArrays, $adValues, $debugTag = 'replaceADDefault') {
logger()->info("Masuk $debugTag", [
'formula' => $formula,
'adArrays' => $adArrays,
'adValues' => $adValues
]);
return preg_replace_callback('/AD([0-9]+)/', function($ad) use ($adArrays, $adValues) {
$adFull = "AD".$ad[1];
$val = $adArrays[$adFull][0] ?? $adValues[$adFull] ?? 0;
if (!is_numeric($val)) {
logger()->warning("AD non-numeric (default), replaced with 1", [
'ad' => $adFull, 'value' => $val
]);
return 1;
}
logger()->info("replaceADDefault: replace", [
'ad' => $adFull, 'value' => $val
]);
return $val;
}, $formula);
}
public function replaceREF(
$formula,
$refParams,
$refContext,
$adArrays,
$adValues,
$adUnits = [],
$debugTag = '',
$as_array = false
) {
logger()->info("Masuk $debugTag", ['formula' => $formula]);
// Hitung jumlah CF di formula
preg_match_all('/REF_CF[0-9]+/i', $formula, $cfMatches);
$cfCount = count(array_unique($cfMatches[0]));
if ($cfCount > 2) {
logger()->error("Formula mengandung lebih dari 2 Conversion Factor (CF): $cfCount", [
'formula' => $formula,
'cf_found' => $cfMatches[0]
]);
throw new \Exception("Formula hanya boleh mengandung maksimal 2 Conversion Factor (CF), ditemukan: $cfCount");
}
// FIX: Use $as_array in closure
return preg_replace_callback('/REF_([A-Z0-9_]+)/', function($mm) use ($refParams, $refContext, $adArrays, $adValues, $adUnits, $as_array) {
$key = $mm[1];
if (!isset($refParams[$key])) {
logger()->error("Mapping untuk REF_$key tidak ditemukan.", [
'key' => $key, 'refParams' => $refParams
]);
throw new \Exception("Mapping untuk REF_$key tidak ditemukan.");
}
$map = $refParams[$key];
// Special handling: Conversion Factor
if (
strtoupper(substr($map['param'], 0, 2)) === 'CF'
|| strtoupper($map['param']) === 'CONVERSION FACTOR'
) {
$adKeyUnit = null;
foreach ($map['context'] as $field => $adKeyC) {
$adKeyUnit = $adKeyC;
break;
}
$unit = $adUnits[$adKeyUnit] ?? null;
if (!$unit) {
logger()->error("Unit untuk Conversion Factor tidak ditemukan di AD.", [
'param' => $map['param'],
'context' => $map['context'],
'adKeyUnit' => $adKeyUnit,
'adUnits' => $adUnits,
'adValues' => $adValues,
'adArrays' => $adArrays,
]);
throw new \Exception("Unit untuk Conversion Factor tidak ditemukan di AD $adKeyUnit.");
}
// Cek kategori unit
$category = $this->unitCategoryMap[strtolower($unit)] ?? null;
logger()->info("CF Detected", [
'unit' => $unit,
'category' => $category,
]);
if (!$category) throw new \Exception("Kategori unit tidak diketahui: $unit");
// Ambil conversion factor value
$q = \App\Models\EmissionFactorReference::where('param', $map['param'])
->whereRaw('LOWER(unit) = ?', [strtolower($unit)]);
$cfValue = $q->value('value');
logger()->info("REF CF Value", [
'param' => $map['param'],
'unit' => $unit,
'cfValue' => $cfValue
]);
if ($cfValue === null) throw new \Exception("CF reference tidak ditemukan (param=$map[param], unit=$unit)");
// Bangun rumus berdasarkan kategori
$formulaCF = '';
if ($category == 'volume') {
$formulaCF = "($cfValue*REF_BERAT_JENIS*REF_NCV)";
} else if ($category == 'mass') {
$formulaCF = "($cfValue*REF_NCV)";
} else if ($category == 'energy') {
$formulaCF = "($cfValue)";
} else {
throw new \Exception("Kategori unit tidak didukung: $category");
}
// Recursively replace reference di rumus baru!
$finalVal = $this->replaceREF($formulaCF, $refParams, $refContext, $adArrays, $adValues, $adUnits, 'CF_RECURSE');
$result = null;
eval("\$result = $finalVal;");
logger()->info("Eval CF Result", [
'formula' => $finalVal,
'result' => $result
]);
return $result;
}
// --- Query reference biasa, ambil dari keterangan/value AD ---
$contextKey = null;
foreach ($map['context'] as $field => $adKeyC) {
$contextKey = $adKeyC;
}
$contextIsArray = $contextKey && isset($adArrays[$contextKey]) && is_array($adArrays[$contextKey]) && count($adArrays[$contextKey]) > 1;
if ($contextIsArray) {
$adVals = $adArrays[$contextKey];
$refVals = [];
foreach ($adVals as $v) {
$q = \App\Models\EmissionFactorReference::where('param', $map['param'])
->where('unit', $map['unit'])
->where('keterangan', $v);
$val = $q->value('value');
if ($val !== null) $refVals[] = $val;
}
if ($as_array) {
logger()->info("REF Array Result for $key", [
'adVals' => $adVals,
'refVals' => $refVals
]);
return implode(',', $refVals);
// return (string)$refVals;
}
$sum = array_sum($refVals);
logger()->info("REF Array SUM for $key", [
'adVals' => $adVals,
'refVals' => $refVals,
'sum' => $sum
]);
// dd(strval($sum == 0 ? 1 : $sum));
return strval($sum == 0 ? 1 : $sum); // <-- Ini kunci!
} else {
// default: ambil single value (seperti sebelumnya)
$where = [];
foreach ($map['context'] as $field => $adKeyC) {
$cv = $adArrays[$adKeyC][0] ?? $adValues[$adKeyC] ?? null;
if ($cv !== null) $where[] = ['keterangan', '=', $cv];
}
logger()->info("QUERY REF Single", [
'param' => $map['param'],
'unit' => $map['unit'],
'where' => $where
]);
$q = \App\Models\EmissionFactorReference::where('param', $map['param'])
->where('unit', $map['unit']);
foreach ($where as $w) $q->where($w[0], $w[2]);
$rv = $q->value('value');
logger()->info("REF Value Result Single", [
'param' => $map['param'],
'unit' => $map['unit'],
'context' => $map['context'],
'where' => $where,
'value' => $rv
]);
return $rv ?? 1;
}
}, $formula);
}
/**
* Parse dan evaluasi semua fungsi SUMPRODUCT pada formula, mendukung konstanta CONST_ADn, ADn, REF_xxx, angka, power, persen, bracket.
* SUMPRODUCT(AD8*AD9*0.8/1000*(1/(1-20%)))
* SUMPRODUCT(CONST_AD2*AD5*...)+SUMPRODUCT(...)
* dst.
* @param string $formula
* @param array $adArrays
* @param array $adValues
* @param array $refParams
* @param array $refContext
* @return string
*/
public function parseSumproductExcelLikeV3($formula, $adArrays, $adValues, $refParams, $refContext)
{
// Jika terdapat lebih dari satu SUMPRODUCT di formula, gunakan versi multi
if (substr_count(strtoupper($formula), 'SUMPRODUCT(') > 1) {
return $this->parseSumproductMultiple($formula, $adArrays, $adValues, $refParams, $refContext);
}
// Regex pattern handle nested bracket dalam SUMPRODUCT (recursively, non-greedy)
$pattern = '/SUMPRODUCT\s*\(((?:[^()]++|(?R))*?)\)/i';
while (preg_match($pattern, $formula)) {
$formula = preg_replace_callback($pattern, function($m) use ($adArrays, $adValues, $refParams, $refContext) {
$inner = $m[1];
$sum = 0;
// Ganti AD dan REF inline
$expr = preg_replace_callback('/AD([0-9]+)/', function($mm) use ($adArrays, $adValues) {
$adKey = 'AD' . $mm[1];
return $adValues[$adKey] ?? $adArrays[$adKey][0] ?? 0;
}, $inner);
$expr = preg_replace_callback('/REF_([A-Z0-9_]+)/', function($mm) use ($refParams, $refContext, $adArrays, $adValues) {
$key = $mm[1];
if (!isset($refParams[$key])) return 0;
$map = $refParams[$key];
$contextKey = null;
foreach ($map['context'] as $field => $adKeyC) {
$contextKey = $adKeyC;
}
$adVal = $contextKey && isset($adValues[$contextKey]) ? $adValues[$contextKey] : ($adArrays[$contextKey][0] ?? null);
$q = \App\Models\EmissionFactorReference::where('param', $map['param'])
->where('unit', $map['unit'])
->where('keterangan', $adVal);
return $q->value('value') ?? 0;
}, $expr);
$expr = preg_replace_callback('/(\d+(?:\.\d+)?)%/', fn($mm) => $mm[1]/100, $expr);
$expr = preg_replace('/([0-9.Ee+\-]+)\s*\^\s*([0-9.Ee+\-]+)/', 'pow($1,$2)', $expr);
try {
$sum = eval('return ' . $expr . ';');
} catch (\Throwable $e) {
logger()->error("[SUMPRODUCT] Eval error: $expr | " . $e->getMessage());
$sum = 0;
}
return (string)$sum;
}, $formula, 1); // replace 1x per loop
}
return $formula;
}
public function parseSumproductMultiple($formula, $adArrays, $adValues, $refParams, $refContext)
{
$pattern = '/SUMPRODUCT\s*\(((?:[^()]++|(?R))*?)\)/i';
return preg_replace_callback($pattern, function ($m) use ($adArrays, $adValues, $refParams, $refContext) {
$sub = 'SUMPRODUCT(' . $m[1] . ')';
return $this->parseSumproductExcelLikeV3($sub, $adArrays, $adValues, $refParams, $refContext);
}, $formula);
}
}