'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); } }