/** * ============================================================ * FUNCTION 5 * Assign Root Positions * ============================================================ * * Assigns r0, r1, and r2 from structural clusters. * * Handles: * - Form II doubled middle radical * - Form III internal long alif * - Form IV initial derivational hamza * - Form V initial derivational ta * - Form VI initial derivational ta plus internal long alif * - Form VII derivational n * - Form VIII inserted or assimilated t-marker * - Form IX contracted and expanded final radical * - Form X ist-prefix * - Imperfect person prefixes * * Important Form VIII example: * * اِضْطَرَبَ * ا + ض + ط + ر + ب * * r0 = ض * vm = ط * r1 = ر * r2 = ب * * Important expanded Form IX example: * * اِحْمَرَرْتُ * ا + ح + م + ر + ر * * r0 = ح * r1 = م * r2 = ر * * @param array $analysis * * @return array */ function ace_assign_root_positions(array $analysis): array { /* * Preserve all fields produced by earlier stages. */ $result = $analysis; /* * Reset only fields owned by this function. */ $result['r0'] = ''; $result['r1'] = ''; $result['r2'] = ''; $result['v1'] = ''; $result['v2'] = ''; $result['r0_marks'] = ''; $result['r1_marks'] = ''; $result['r2_marks'] = ''; $result['r0_index'] = null; $result['r1_index'] = null; $result['r2_index'] = null; $result['long_a_after_r0'] = false; $result['form3_long_a_index'] = null; /* * Diagnostic field owned by this stage. */ $result['root_assignment_pattern'] = ''; $result['surface_root_clusters'] = array(); $result['person_prefix'] = ''; $result['person_prefix_index'] = null; /* * ------------------------------------------------------------ * Get the best available structural cluster collection. * ------------------------------------------------------------ */ $clusters = array(); if ( isset($analysis['structural_clusters']) && is_array($analysis['structural_clusters']) ) { $clusters = $analysis['structural_clusters']; } elseif ( isset($analysis['clusters']) && is_array($analysis['clusters']) ) { $clusters = $analysis['clusters']; } if (empty($clusters)) { return $result; } /* * ------------------------------------------------------------ * Normalize cluster records. * ------------------------------------------------------------ */ $normalizedClusters = array(); foreach ($clusters as $index => $cluster) { if (is_array($cluster)) { $letter = isset($cluster['letter']) ? trim((string) $cluster['letter']) : ''; $marks = isset($cluster['marks']) ? (string) $cluster['marks'] : ''; $text = isset($cluster['text']) ? (string) $cluster['text'] : $letter . $marks; } else { $letter = trim((string) $cluster); $marks = ''; $text = $letter; } if ($letter === '') { continue; } $normalizedClusters[] = array( 'letter' => $letter, 'marks' => $marks, 'text' => $text, 'index' => $index, ); } if (empty($normalizedClusters)) { return $result; } $working = $normalizedClusters; /* * ------------------------------------------------------------ * Read evidence produced by preceding stages. * ------------------------------------------------------------ */ $vn = isset($analysis['vn']) ? trim((string) $analysis['vn']) : ''; $vm = isset($analysis['vm']) ? trim((string) $analysis['vm']) : ''; $fs = isset($analysis['fs']) ? trim((string) $analysis['fs']) : ''; $detectedForm = ''; if (isset($analysis['form'])) { $detectedForm = strtoupper( trim((string) $analysis['form']) ); } elseif (isset($analysis['detected_form'])) { $detectedForm = strtoupper( trim((string) $analysis['detected_form']) ); } elseif (isset($analysis['selected_form'])) { $detectedForm = strtoupper( trim((string) $analysis['selected_form']) ); } /* * ------------------------------------------------------------ * Remove an imperfect person prefix. * ------------------------------------------------------------ * * Prefixes are not removed from the stored stem. They are removed * only from this local working collection so that the radical * positions can be assigned. */ /* * Do not depend on only one upstream flag. Some test rows carry * imperfect evidence in the initial-group or suffix metadata while * is_imperfect_group is absent or stale. */ $isImperfect = !empty($analysis['is_imperfect_group']) || !empty($analysis['is_imperfect']) || ( isset($analysis['initial_group_value']) && trim((string) $analysis['initial_group_value']) !== '' ) || ( isset($analysis['matched_suffix_entry']) && strpos( (string) $analysis['matched_suffix_entry'], 'families=imperfect' ) !== false ); /* * A contracted weak imperfect may contain only three surface * clusters, for example: * * يَعِدْ = ي + ع + د * يَدْعُ = ي + د + ع * * The person prefix must still be removed locally. If fewer than * three radical clusters remain, this function leaves the root * incomplete instead of promoting the person prefix to r0. */ if ( $isImperfect && count($working) >= 3 && isset($working[0]) && in_array( $working[0]['letter'], array('ي', 'ت', 'أ', 'ن'), true ) ) { $result['person_prefix'] = $working[0]['letter']; $result['person_prefix_index'] = $working[0]['index']; array_shift($working); } if (empty($working)) { return $result; } /* * ------------------------------------------------------------ * Create a structural sequence without an initial support alif. * ------------------------------------------------------------ * * This applies to the perfect/imperative structures of Forms * VII, VIII, IX, and X: * * ا + ن + ... * ا + C1 + ت/ط/د + ... * ا + C1 + C2 + C3ّ * ا + س + ت + ... * * The alif is removed only from this local structural sequence. */ $sequence = $working; $removedSupportAlif = false; if ( count($sequence) >= 4 && isset($sequence[0]) && in_array( $sequence[0]['letter'], array('ا', 'ٱ'), true ) ) { array_shift($sequence); $removedSupportAlif = true; } /* * ------------------------------------------------------------ * Form X * ------------------------------------------------------------ * * Perfect/imperative after support-alif removal: * * س + ت + C1 + C2 + C3 * * Imperfect after person-prefix removal: * * س + ت + C1 + C2 + C3 */ $looksLikeFormX = count($sequence) >= 5 && isset($sequence[0], $sequence[1]) && $sequence[0]['letter'] === 'س' && $sequence[1]['letter'] === 'ت'; $hasFormXEvidence = $detectedForm === 'X' || $detectedForm === '10' || $fs === 'است' || $fs === 'ست' || $looksLikeFormX; if ($hasFormXEvidence && count($sequence) >= 5) { $rootClusters = array_slice($sequence, -3); $r0Cluster = $rootClusters[0]; $r1Cluster = $rootClusters[1]; $r2Cluster = $rootClusters[2]; $result['r0'] = $r0Cluster['letter']; $result['r1'] = $r1Cluster['letter']; $result['r2'] = $r2Cluster['letter']; $result['r0_marks'] = $r0Cluster['marks']; $result['r1_marks'] = $r1Cluster['marks']; $result['r2_marks'] = $r2Cluster['marks']; $result['r0_index'] = $r0Cluster['index']; $result['r1_index'] = $r1Cluster['index']; $result['r2_index'] = $r2Cluster['index']; $result['root_assignment_pattern'] = 'form_x'; return $result; } /* * ------------------------------------------------------------ * Form VII * ------------------------------------------------------------ * * After removal of the support alif or imperfect prefix: * * ن + C1 + C2 + C3 */ $looksLikeFormVII = count($sequence) === 4 && isset($sequence[0]) && $sequence[0]['letter'] === 'ن'; $hasFormVIIEvidence = $detectedForm === 'VII' || $detectedForm === '7' || $fs === 'ن' || $looksLikeFormVII; if ($hasFormVIIEvidence && count($sequence) === 4) { $r0Cluster = $sequence[1]; $r1Cluster = $sequence[2]; $r2Cluster = $sequence[3]; $result['r0'] = $r0Cluster['letter']; $result['r1'] = $r1Cluster['letter']; $result['r2'] = $r2Cluster['letter']; $result['r0_marks'] = $r0Cluster['marks']; $result['r1_marks'] = $r1Cluster['marks']; $result['r2_marks'] = $r2Cluster['marks']; $result['r0_index'] = $r0Cluster['index']; $result['r1_index'] = $r1Cluster['index']; $result['r2_index'] = $r2Cluster['index']; $result['root_assignment_pattern'] = 'form_vii'; return $result; } /* * ------------------------------------------------------------ * Form VIII * ------------------------------------------------------------ * * After removal of the support alif or imperfect prefix: * * C1 + ت + C2 + C3 * C1 + ط + C2 + C3 * C1 + د + C2 + C3 * * The second structural consonant is the Form VIII marker, * not the first radical. * * Example: * * ض + ط + ر + ب * * r0 = ض * vm = ط * r1 = ر * r2 = ب */ $formVIIIMarkers = array('ت', 'ط', 'د'); $looksLikeFormVIII = count($sequence) === 4 && isset($sequence[1]) && in_array( $sequence[1]['letter'], $formVIIIMarkers, true ); $hasFormVIIIEvidence = $detectedForm === 'VIII' || $detectedForm === '8' || in_array($vm, $formVIIIMarkers, true) || $looksLikeFormVIII; if ($hasFormVIIIEvidence && count($sequence) === 4) { $r0Cluster = $sequence[0]; $vmCluster = $sequence[1]; $r1Cluster = $sequence[2]; $r2Cluster = $sequence[3]; $result['r0'] = $r0Cluster['letter']; $result['r1'] = $r1Cluster['letter']; $result['r2'] = $r2Cluster['letter']; $result['r0_marks'] = $r0Cluster['marks']; $result['r1_marks'] = $r1Cluster['marks']; $result['r2_marks'] = $r2Cluster['marks']; $result['r0_index'] = $r0Cluster['index']; $result['r1_index'] = $r1Cluster['index']; $result['r2_index'] = $r2Cluster['index']; /* * Preserve an earlier vm value when present. Otherwise record * the structurally detected Form VIII marker. */ if ($vm === '') { $result['vm'] = $vmCluster['letter']; } $result['vm_index'] = $vmCluster['index']; $result['root_assignment_pattern'] = 'form_viii'; return $result; } /* * ------------------------------------------------------------ * Form IX — expanded final radical * ------------------------------------------------------------ * * Expanded structure: * * C1 + C2 + C3 + C3 * * Examples: * * اِحْمَرَرْتُ * يَحْمَرَرْنَ * * The final two consonants represent one doubled third radical. */ $looksLikeExpandedFormIX = count($sequence) === 4 && isset($sequence[2], $sequence[3]) && $sequence[2]['letter'] === $sequence[3]['letter']; $hasFormIXEvidence = $detectedForm === 'IX' || $detectedForm === '9' || !empty($analysis['r2_repeat']) || $looksLikeExpandedFormIX; if ($hasFormIXEvidence && $looksLikeExpandedFormIX) { $r0Cluster = $sequence[0]; $r1Cluster = $sequence[1]; $r2Cluster = $sequence[2]; $result['r0'] = $r0Cluster['letter']; $result['r1'] = $r1Cluster['letter']; $result['r2'] = $r2Cluster['letter']; $result['r0_marks'] = $r0Cluster['marks']; $result['r1_marks'] = $r1Cluster['marks']; $result['r2_marks'] = $r2Cluster['marks']; $result['r0_index'] = $r0Cluster['index']; $result['r1_index'] = $r1Cluster['index']; $result['r2_index'] = $r2Cluster['index']; $result['r2_repeat'] = true; $result['r2_repeat_index'] = $sequence[3]['index']; $result['root_assignment_pattern'] = 'form_ix_expanded'; return $result; } /* * ------------------------------------------------------------ * Form IX — contracted shadda * ------------------------------------------------------------ * * Contracted structure: * * C1 + C2 + C3ّ */ if ( $hasFormIXEvidence && count($sequence) === 3 && isset($sequence[2]) && strpos( (string) $sequence[2]['marks'], 'ّ' ) !== false ) { $r0Cluster = $sequence[0]; $r1Cluster = $sequence[1]; $r2Cluster = $sequence[2]; $result['r0'] = $r0Cluster['letter']; $result['r1'] = $r1Cluster['letter']; $result['r2'] = $r2Cluster['letter']; $result['r0_marks'] = $r0Cluster['marks']; $result['r1_marks'] = $r1Cluster['marks']; $result['r2_marks'] = $r2Cluster['marks']; $result['r0_index'] = $r0Cluster['index']; $result['r1_index'] = $r1Cluster['index']; $result['r2_index'] = $r2Cluster['index']; $result['r2_repeat'] = true; $result['root_assignment_pattern'] = 'form_ix_contracted'; return $result; } /* * ------------------------------------------------------------ * Form IV * ------------------------------------------------------------ * * أ + C1 + C2 + C3 * * Require Form IV evidence so that a genuine initial hamza * radical is not removed accidentally. */ $isFormIV = $detectedForm === 'IV' || $detectedForm === '4' || $vn === 'أ'; if ( $isFormIV && count($working) === 4 && isset($working[0]) && in_array( $working[0]['letter'], array('أ', 'إ', 'ا'), true ) ) { array_shift($working); } /* * ------------------------------------------------------------ * Forms V and VI * ------------------------------------------------------------ * * Form V: * * ت + C1 + C2ّ + C3 * * Form VI: * * ت + C1 + ا + C2 + C3 */ if ( isset($working[0]) && $working[0]['letter'] === 'ت' ) { $looksLikeFormVI = count($working) === 5 && isset($working[2]) && $working[2]['letter'] === 'ا'; $looksLikeFormV = false; if ( count($working) === 4 && isset($working[2]) ) { $looksLikeFormV = strpos( (string) $working[2]['marks'], 'ّ' ) !== false; } $hasFormVOrVIEvidence = $detectedForm === 'V' || $detectedForm === '5' || $detectedForm === 'VI' || $detectedForm === '6' || $vn === 'ت' || $looksLikeFormV || $looksLikeFormVI; if ($hasFormVOrVIEvidence) { array_shift($working); } } /* * ------------------------------------------------------------ * Form III or Form VI internal long alif * ------------------------------------------------------------ * * Remaining structure: * * C1 + ا + C2 + C3 */ if ( count($working) === 4 && isset($working[1]) && $working[1]['letter'] === 'ا' ) { $r0Cluster = $working[0]; $longACluster = $working[1]; $r1Cluster = $working[2]; $r2Cluster = $working[3]; $result['r0'] = $r0Cluster['letter']; $result['v1'] = $longACluster['letter']; $result['r1'] = $r1Cluster['letter']; $result['r2'] = $r2Cluster['letter']; $result['r0_marks'] = $r0Cluster['marks']; $result['r1_marks'] = $r1Cluster['marks']; $result['r2_marks'] = $r2Cluster['marks']; $result['r0_index'] = $r0Cluster['index']; $result['r1_index'] = $r1Cluster['index']; $result['r2_index'] = $r2Cluster['index']; $result['long_a_after_r0'] = true; $result['form3_long_a_index'] = $longACluster['index']; $result['root_assignment_pattern'] = $detectedForm === 'VI' || $detectedForm === '6' ? 'form_vi' : 'form_iii'; return $result; } /* * ------------------------------------------------------------ * Normal triliteral assignment * ------------------------------------------------------------ * * This also correctly handles Form II because the doubled * middle radical remains one cluster containing a shadda. */ if (count($working) < 3) { /* * Do not invent a triliteral root from a person prefix or from * only two surviving surface radicals. Weak-root reconstruction * belongs to the later root-candidate stages. */ $result['root_assignment_pattern'] = count($working) === 2 ? 'contracted_root_incomplete' : 'insufficient_root_clusters'; $result['surface_root_clusters'] = array_map( static function (array $cluster): string { return $cluster['letter']; }, $working ); return $result; } $rootClusters = array_slice($working, -3); $r0Cluster = $rootClusters[0]; $r1Cluster = $rootClusters[1]; $r2Cluster = $rootClusters[2]; $result['r0'] = $r0Cluster['letter']; $result['r1'] = $r1Cluster['letter']; $result['r2'] = $r2Cluster['letter']; $result['r0_marks'] = $r0Cluster['marks']; $result['r1_marks'] = $r1Cluster['marks']; $result['r2_marks'] = $r2Cluster['marks']; $result['r0_index'] = $r0Cluster['index']; $result['r1_index'] = $r1Cluster['index']; $result['r2_index'] = $r2Cluster['index']; $result['root_assignment_pattern'] = 'normal_triliteral'; return $result; }