@@ -9,6 +9,8 @@ import { MetricsResult, ButtonMetrics, ComparisonResult } from './types';
99import { SentenceAnalyzer } from './sentence' ;
1010import { VocabularyAnalyzer } from './vocabulary' ;
1111import { ReferenceLoader } from '../reference/index' ;
12+ import { spellingEffort , predictionEffort } from './effort' ;
13+ import { MetricsOptions } from './types' ;
1214
1315export class ComparisonAnalyzer {
1416 private vocabAnalyzer : VocabularyAnalyzer ;
@@ -37,7 +39,7 @@ export class ComparisonAnalyzer {
3739 options ?: {
3840 includeSentences ?: boolean ;
3941 locale ?: string ;
40- }
42+ } & Partial < MetricsOptions >
4143 ) : ComparisonResult {
4244 // Create base result from target
4345 const baseResult = { ...targetResult } ;
@@ -102,7 +104,8 @@ export class ComparisonAnalyzer {
102104 const careComponents = this . calculateCareComponents (
103105 targetResult ,
104106 compareResult ,
105- overlappingWords
107+ overlappingWords ,
108+ options
106109 ) ;
107110
108111 // Analyze high/low effort words
@@ -269,8 +272,61 @@ export class ComparisonAnalyzer {
269272 private calculateCareComponents (
270273 targetResult : MetricsResult ,
271274 compareResult : MetricsResult ,
272- _overlappingWords : string [ ]
275+ _overlappingWords : string [ ] ,
276+ options ?: {
277+ includeSentences ?: boolean ;
278+ locale ?: string ;
279+ } & Partial < MetricsOptions >
273280 ) : ComparisonResult [ 'care_components' ] {
281+ // Load common words with baseline efforts (matching Ruby line 527-534)
282+ const commonWordsData = this . referenceLoader . loadCommonWords ( ) ;
283+ const commonWords = new Map < string , number > ( ) ;
284+ commonWordsData . words . forEach ( ( word : string ) => {
285+ commonWords . set ( word . toLowerCase ( ) , commonWordsData . efforts [ word ] || 0 ) ;
286+ } ) ;
287+
288+ // Determine prediction settings (default: use common words efforts, not prediction)
289+ const usePrediction = options ?. usePrediction || false ; // Default FALSE (use common words)
290+ const predictionSelections = options ?. predictionSelections || 1.5 ;
291+ const debugMode = process . env . DEBUG_METRICS === 'true' ;
292+
293+ // Helper function to calculate fallback effort
294+ const getFallbackEffort = (
295+ word : string ,
296+ hasPrediction : boolean ,
297+ spellingBaseEffort ?: number
298+ ) : number => {
299+ const wordLower = word . toLowerCase ( ) ;
300+
301+ // Check common words efforts first (matching Ruby line 533)
302+ if ( commonWords . has ( wordLower ) ) {
303+ const effort = commonWords . get ( wordLower ) ;
304+ return effort !== undefined ? effort : spellingEffort ( word , 10 , 2.5 ) ;
305+ }
306+
307+ // If usePrediction is true and prediction is available, use prediction
308+ if ( usePrediction && hasPrediction && spellingBaseEffort !== undefined ) {
309+ return predictionEffort ( spellingBaseEffort , 2.5 , predictionSelections , 2 ) ;
310+ }
311+
312+ // Fallback to manual spelling (matching Ruby spelling_effort: 10 + word.length * 2.5)
313+ return spellingEffort ( word , 10 , 2.5 ) ;
314+ } ;
315+
316+ // Debug: Check settings
317+ const targetHasPrediction =
318+ targetResult . has_dynamic_prediction && targetResult . spelling_effort_base !== undefined ;
319+ const _compareHasPrediction =
320+ compareResult . has_dynamic_prediction && compareResult . spelling_effort_base !== undefined ;
321+ if ( debugMode ) {
322+ console . log ( `\n🔍 DEBUG Fallback Effort Settings:` ) ;
323+ console . log ( ` Common words loaded: ${ commonWords . size } ` ) ;
324+ console . log ( ` usePrediction option: ${ usePrediction } ` ) ;
325+ console . log ( ` Target has prediction capability: ${ targetHasPrediction } ` ) ;
326+ console . log (
327+ ` Target spelling_base: ${ targetResult . spelling_effort_base ?. toFixed ( 2 ) || 'undefined' } `
328+ ) ;
329+ }
274330 // Create word maps with normalized keys
275331 const targetWords = new Map < string , ButtonMetrics > ( ) ;
276332 targetResult . buttons . forEach ( ( btn ) => {
@@ -293,68 +349,200 @@ export class ComparisonAnalyzer {
293349 // Load reference data
294350 const coreLists = this . referenceLoader . loadCoreLists ( ) ;
295351 const fringe = this . referenceLoader . loadFringe ( ) ;
352+ const commonFringe = this . referenceLoader . loadCommonFringe ( ) ;
296353 const sentences = this . referenceLoader . loadSentences ( ) ;
297354
298- // Calculate core coverage
355+ // Calculate core coverage and effort (matching Ruby lines 609-647)
299356 let coreCount = 0 ;
300357 let compCoreCount = 0 ;
358+ let targetCoreEffort = 0 ;
359+ let compCoreEffort = 0 ;
301360 const allCoreWords = new Set < string > ( ) ;
302361 coreLists . forEach ( ( list ) => {
303362 list . words . forEach ( ( word ) => allCoreWords . add ( word . toLowerCase ( ) ) ) ;
304363 } ) ;
305364
306365 allCoreWords . forEach ( ( word ) => {
307366 const key = this . normalize ( word ) ;
308- if ( targetWords . has ( key ) ) coreCount ++ ;
309- if ( compareWords . has ( key ) ) compCoreCount ++ ;
367+ const targetBtn = targetWords . get ( key ) ;
368+ const compareBtn = compareWords . get ( key ) ;
369+
370+ if ( targetBtn ) {
371+ coreCount ++ ;
372+ targetCoreEffort += targetBtn . effort ;
373+ } else {
374+ // Fallback to spelling or prediction effort
375+ targetCoreEffort += getFallbackEffort (
376+ word ,
377+ targetResult . has_dynamic_prediction || false ,
378+ targetResult . spelling_effort_base
379+ ) ;
380+ }
381+
382+ if ( compareBtn ) {
383+ compCoreCount ++ ;
384+ compCoreEffort += compareBtn . effort ;
385+ } else {
386+ compCoreEffort += getFallbackEffort (
387+ word ,
388+ compareResult . has_dynamic_prediction || false ,
389+ compareResult . spelling_effort_base
390+ ) ;
391+ }
310392 } ) ;
311393
312- // Calculate sentence construction effort
313- let sentenceEffort = 0 ;
314- let compSentenceEffort = 0 ;
315- let sentenceWordCount = 0 ;
394+ const avgCoreEffort = allCoreWords . size > 0 ? targetCoreEffort / allCoreWords . size : 0 ;
395+ const avgCompCoreEffort = allCoreWords . size > 0 ? compCoreEffort / allCoreWords . size : 0 ;
396+
397+ // Calculate core component scores (matching Ruby lines 644-647)
398+ const coreScore = avgCoreEffort * 5.0 ;
399+ const compCoreScore = avgCompCoreEffort * 5.0 ;
400+
401+ // Calculate sentence construction effort (matching Ruby lines 654-668)
402+ const sentenceEfforts : number [ ] = [ ] ;
403+ const compSentenceEfforts : number [ ] = [ ] ;
316404
317405 sentences . forEach ( ( words ) => {
406+ let targetSentenceEffort = 0 ;
407+ let compSentenceEffort = 0 ;
408+
318409 words . forEach ( ( word ) => {
319410 const key = this . normalize ( word ) ;
320411 const targetBtn = targetWords . get ( key ) ;
321412 const compareBtn = compareWords . get ( key ) ;
322413
323414 if ( targetBtn ) {
324- sentenceEffort += targetBtn . effort ;
415+ targetSentenceEffort += targetBtn . effort ;
325416 } else {
326- sentenceEffort += 10 + word . length * 2.5 ; // Spelling effort
417+ targetSentenceEffort += getFallbackEffort (
418+ word ,
419+ targetResult . has_dynamic_prediction || false ,
420+ targetResult . spelling_effort_base
421+ ) ;
327422 }
328423
329424 if ( compareBtn ) {
330425 compSentenceEffort += compareBtn . effort ;
331426 } else {
332- compSentenceEffort += 10 + word . length * 2.5 ;
427+ compSentenceEffort += getFallbackEffort (
428+ word ,
429+ compareResult . has_dynamic_prediction || false ,
430+ compareResult . spelling_effort_base
431+ ) ;
333432 }
334-
335- sentenceWordCount ++ ;
336433 } ) ;
434+
435+ // Average effort per sentence (matching Ruby line 657)
436+ sentenceEfforts . push ( targetSentenceEffort / words . length ) ;
437+ compSentenceEfforts . push ( compSentenceEffort / words . length ) ;
337438 } ) ;
338439
339- const avgSentenceEffort = sentenceWordCount > 0 ? sentenceEffort / sentenceWordCount : 0 ;
440+ const avgSentenceEffort =
441+ sentenceEfforts . length > 0
442+ ? sentenceEfforts . reduce ( ( a , b ) => a + b , 0 ) / sentenceEfforts . length
443+ : 0 ;
340444 const compAvgSentenceEffort =
341- sentenceWordCount > 0 ? compSentenceEffort / sentenceWordCount : 0 ;
445+ compSentenceEfforts . length > 0
446+ ? compSentenceEfforts . reduce ( ( a , b ) => a + b , 0 ) / compSentenceEfforts . length
447+ : 0 ;
448+
449+ // Sentence component scores (matching Ruby line 665-668)
450+ const sentenceScore = avgSentenceEffort * 3.0 ;
451+ const compSentenceScore = compAvgSentenceEffort * 3.0 ;
342452
343- // Calculate fringe coverage
453+ // Calculate fringe effort (matching Ruby lines 670-687)
454+ const fringeEfforts : number [ ] = [ ] ;
455+ const compFringeEfforts : number [ ] = [ ] ;
344456 let fringeCount = 0 ;
345457 let compFringeCount = 0 ;
458+
459+ fringe . forEach ( ( word : string ) => {
460+ const key = this . normalize ( word ) ;
461+ const targetBtn = targetWords . get ( key ) ;
462+ const compareBtn = compareWords . get ( key ) ;
463+
464+ if ( targetBtn ) {
465+ fringeEfforts . push ( targetBtn . effort ) ;
466+ fringeCount ++ ;
467+ } else {
468+ fringeEfforts . push (
469+ getFallbackEffort (
470+ word ,
471+ targetResult . has_dynamic_prediction || false ,
472+ targetResult . spelling_effort_base
473+ )
474+ ) ;
475+ }
476+
477+ if ( compareBtn ) {
478+ compFringeEfforts . push ( compareBtn . effort ) ;
479+ compFringeCount ++ ;
480+ } else {
481+ compFringeEfforts . push (
482+ getFallbackEffort (
483+ word ,
484+ compareResult . has_dynamic_prediction || false ,
485+ compareResult . spelling_effort_base
486+ )
487+ ) ;
488+ }
489+ } ) ;
490+
491+ const avgFringeEffort =
492+ fringeEfforts . length > 0
493+ ? fringeEfforts . reduce ( ( a , b ) => a + b , 0 ) / fringeEfforts . length
494+ : 0 ;
495+ const avgCompFringeEffort =
496+ compFringeEfforts . length > 0
497+ ? compFringeEfforts . reduce ( ( a , b ) => a + b , 0 ) / compFringeEfforts . length
498+ : 0 ;
499+
500+ // Fringe component scores (matching Ruby line 684-687)
501+ const fringeScore = avgFringeEffort * 2.0 ;
502+ const compFringeScore = avgCompFringeEffort * 2.0 ;
503+
504+ // Calculate common fringe effort (matching Ruby lines 689-705)
505+ const commonFringeEfforts : number [ ] = [ ] ;
506+ const compCommonFringeEfforts : number [ ] = [ ] ;
346507 let commonFringeCount = 0 ;
347508
348- fringe . forEach ( ( word ) => {
509+ commonFringe . forEach ( ( word : string ) => {
349510 const key = this . normalize ( word ) ;
350- const inTarget = targetWords . has ( key ) ;
351- const inCompare = compareWords . has ( key ) ;
511+ const targetBtn = targetWords . get ( key ) ;
512+ const compareBtn = compareWords . get ( key ) ;
352513
353- if ( inTarget ) fringeCount ++ ;
354- if ( inCompare ) compFringeCount ++ ;
355- if ( inTarget && inCompare ) commonFringeCount ++ ;
514+ if ( targetBtn && compareBtn ) {
515+ commonFringeEfforts . push ( targetBtn . effort ) ;
516+ compCommonFringeEfforts . push ( compareBtn . effort ) ;
517+ commonFringeCount ++ ;
518+ }
356519 } ) ;
357520
521+ const avgCommonFringeEffort =
522+ commonFringeEfforts . length > 0
523+ ? commonFringeEfforts . reduce ( ( a , b ) => a + b , 0 ) / commonFringeEfforts . length
524+ : 0 ;
525+ const avgCompCommonFringeEffort =
526+ compCommonFringeEfforts . length > 0
527+ ? compCommonFringeEfforts . reduce ( ( a , b ) => a + b , 0 ) / compCommonFringeEfforts . length
528+ : 0 ;
529+
530+ // Common fringe component scores (matching Ruby line 702-705)
531+ const commonFringeScore = avgCommonFringeEffort * 1.0 ;
532+ const compCommonFringeScore = avgCompCommonFringeEffort * 1.0 ;
533+
534+ // Calculate total CARE effort tally (matching Ruby lines 707-708)
535+ const PLACEHOLDER = 70 ;
536+ const targetEffortTally =
537+ coreScore + sentenceScore + fringeScore + commonFringeScore + PLACEHOLDER ;
538+ const compEffortTally =
539+ compCoreScore + compSentenceScore + compFringeScore + compCommonFringeScore + PLACEHOLDER ;
540+
541+ // Calculate final CARE scores (matching Ruby line 710-711)
542+ // res[:target_effort_score] = [0.0, 350.0 - target_effort_tally].max
543+ const careScore = Math . max ( 0 , 350.0 - targetEffortTally ) ;
544+ const compCareScore = Math . max ( 0 , 350.0 - compEffortTally ) ;
545+
358546 return {
359547 core : coreCount ,
360548 comp_core : compCoreCount ,
@@ -364,6 +552,9 @@ export class ComparisonAnalyzer {
364552 comp_fringe : compFringeCount ,
365553 common_fringe : commonFringeCount ,
366554 comp_common_fringe : commonFringeCount ,
555+ // New composite CARE scores
556+ care_score : careScore ,
557+ comp_care_score : compCareScore ,
367558 } ;
368559 }
369560
0 commit comments