@@ -674,6 +674,49 @@ class SlotExtractor {
674674
675675 // MARK: - Time Normalization Helper
676676
677+ /// Converts word numbers to digits
678+ /// Examples: "three" -> "3", "fifteen" -> "15", "twenty five" -> "25"
679+ private func convertWordToNumber( _ text: String ) -> String {
680+ let wordToDigit : [ String : String ] = [
681+ " zero " : " 0 " , " one " : " 1 " , " two " : " 2 " , " three " : " 3 " , " four " : " 4 " ,
682+ " five " : " 5 " , " six " : " 6 " , " seven " : " 7 " , " eight " : " 8 " , " nine " : " 9 " ,
683+ " ten " : " 10 " , " eleven " : " 11 " , " twelve " : " 12 " , " thirteen " : " 13 " ,
684+ " fourteen " : " 14 " , " fifteen " : " 15 " , " sixteen " : " 16 " , " seventeen " : " 17 " ,
685+ " eighteen " : " 18 " , " nineteen " : " 19 " , " twenty " : " 20 " , " thirty " : " 30 " ,
686+ " forty " : " 40 " , " fifty " : " 50 " , " sixty " : " 60 " , " seventy " : " 70 " ,
687+ " eighty " : " 80 " , " ninety " : " 90 " ,
688+ " a " : " 1 " , " an " : " 1 " , " half " : " 0.5 " , " quarter " : " 0.25 "
689+ ]
690+
691+ var result = text. lowercased ( )
692+
693+ // Handle compound numbers like "twenty five" -> "25"
694+ let compoundPattern = " \\ b(twenty|thirty|forty|fifty|sixty|seventy|eighty|ninety)[ \\ s-]+(one|two|three|four|five|six|seven|eight|nine) \\ b "
695+ if let regex = try ? NSRegularExpression ( pattern: compoundPattern, options: [ . caseInsensitive] ) {
696+ let matches = regex. matches ( in: result, range: NSRange ( result. startIndex... , in: result) )
697+ for match in matches. reversed ( ) {
698+ if let tensRange = Range ( match. range ( at: 1 ) , in: result) ,
699+ let onesRange = Range ( match. range ( at: 2 ) , in: result) ,
700+ let fullRange = Range ( match. range, in: result) {
701+ let tens = Int ( wordToDigit [ String ( result [ tensRange] ) ] ?? " 0 " ) ?? 0
702+ let ones = Int ( wordToDigit [ String ( result [ onesRange] ) ] ?? " 0 " ) ?? 0
703+ let sum = tens + ones
704+ result. replaceSubrange ( fullRange, with: String ( sum) )
705+ }
706+ }
707+ }
708+
709+ // Replace simple word numbers
710+ for (word, digit) in wordToDigit {
711+ let pattern = " \\ b \( word) \\ b "
712+ if let regex = try ? NSRegularExpression ( pattern: pattern, options: [ . caseInsensitive] ) {
713+ result = regex. stringByReplacingMatches ( in: result, range: NSRange ( result. startIndex... , in: result) , withTemplate: digit)
714+ }
715+ }
716+
717+ return result
718+ }
719+
677720 /// Normalizes various time formats to 24-hour HH:MM format
678721 /// Examples:
679722 /// - "730" -> "07:30"
@@ -828,7 +871,9 @@ class SlotExtractor {
828871 /// - "1:30" -> 90 (seconds, interpreted as MM:SS)
829872 /// - "2:30:45" -> 9045 (seconds, HH:MM:SS)
830873 private func normalizeTimerDuration( _ text: String , matchedValue: String ) -> Int {
831- let cleanText = text. lowercased ( )
874+ // Convert word numbers to digits first
875+ let cleanText = convertWordToNumber ( text. lowercased ( ) )
876+ let cleanMatchedValue = convertWordToNumber ( matchedValue. lowercased ( ) )
832877
833878 // Check for combined duration patterns first
834879
@@ -871,30 +916,30 @@ class SlotExtractor {
871916
872917 // HH:MM:SS format
873918 if let regex = try ? NSRegularExpression ( pattern: " ^( \\ d+):( \\ d+):( \\ d+)$ " ) ,
874- let match = regex. firstMatch ( in: matchedValue . trimmingCharacters ( in: . whitespaces) , range: NSRange ( matchedValue . startIndex... , in: matchedValue ) ) {
875- if let hoursRange = Range ( match. range ( at: 1 ) , in: matchedValue ) ,
876- let minutesRange = Range ( match. range ( at: 2 ) , in: matchedValue ) ,
877- let secondsRange = Range ( match. range ( at: 3 ) , in: matchedValue ) {
878- let hours = Int ( matchedValue [ hoursRange] ) ?? 0
879- let minutes = Int ( matchedValue [ minutesRange] ) ?? 0
880- let seconds = Int ( matchedValue [ secondsRange] ) ?? 0
919+ let match = regex. firstMatch ( in: cleanMatchedValue . trimmingCharacters ( in: . whitespaces) , range: NSRange ( cleanMatchedValue . startIndex... , in: cleanMatchedValue ) ) {
920+ if let hoursRange = Range ( match. range ( at: 1 ) , in: cleanMatchedValue ) ,
921+ let minutesRange = Range ( match. range ( at: 2 ) , in: cleanMatchedValue ) ,
922+ let secondsRange = Range ( match. range ( at: 3 ) , in: cleanMatchedValue ) {
923+ let hours = Int ( cleanMatchedValue [ hoursRange] ) ?? 0
924+ let minutes = Int ( cleanMatchedValue [ minutesRange] ) ?? 0
925+ let seconds = Int ( cleanMatchedValue [ secondsRange] ) ?? 0
881926 return hours * 3600 + minutes * 60 + seconds
882927 }
883928 }
884929
885930 // MM:SS format (assuming minutes:seconds for timer)
886931 if let regex = try ? NSRegularExpression ( pattern: " ^( \\ d+):( \\ d+)$ " ) ,
887- let match = regex. firstMatch ( in: matchedValue . trimmingCharacters ( in: . whitespaces) , range: NSRange ( matchedValue . startIndex... , in: matchedValue ) ) {
888- if let minutesRange = Range ( match. range ( at: 1 ) , in: matchedValue ) ,
889- let secondsRange = Range ( match. range ( at: 2 ) , in: matchedValue ) {
890- let minutes = Int ( matchedValue [ minutesRange] ) ?? 0
891- let seconds = Int ( matchedValue [ secondsRange] ) ?? 0
932+ let match = regex. firstMatch ( in: cleanMatchedValue . trimmingCharacters ( in: . whitespaces) , range: NSRange ( cleanMatchedValue . startIndex... , in: cleanMatchedValue ) ) {
933+ if let minutesRange = Range ( match. range ( at: 1 ) , in: cleanMatchedValue ) ,
934+ let secondsRange = Range ( match. range ( at: 2 ) , in: cleanMatchedValue ) {
935+ let minutes = Int ( cleanMatchedValue [ minutesRange] ) ?? 0
936+ let seconds = Int ( cleanMatchedValue [ secondsRange] ) ?? 0
892937 return minutes * 60 + seconds
893938 }
894939 }
895940
896941 // Check for single unit patterns
897- let cleanedValue = matchedValue . replacingOccurrences ( of: " [^ \\ d.] " , with: " " , options: . regularExpression)
942+ let cleanedValue = cleanMatchedValue . replacingOccurrences ( of: " [^ \\ d.] " , with: " " , options: . regularExpression)
898943 guard let value = Double ( cleanedValue) else { return 0 }
899944
900945 if cleanText. range ( of: " \\ b(?:hours?|hrs?|hr|h) \\ b " , options: . regularExpression) != nil {
@@ -978,10 +1023,10 @@ class SlotExtractor {
9781023 " \\ b( \\ d{1,2}(?:: \\ d{2})?[ \\ . \\ s]*([ap])(?: \\ .? \\ s*m(?: \\ .?)?)?) \\ b " ,
9791024 " \\ b( \\ d{3,4}[ \\ . \\ s]*([ap])(?: \\ .? \\ s*m(?: \\ .?)?)?) \\ b " , // For "1030 pm", "630.pm" format
9801025
981- // Duration patterns for timers
982- " \\ b( \\ d+(?: \\ . \\ d+)?) \\ s*(?:hours?|hrs?|hr|h) \\ b " ,
983- " \\ b( \\ d+(?: \\ . \\ d+)?) \\ s*(?:minutes?|mins?|min|m) \\ b " ,
984- " \\ b( \\ d+(?: \\ . \\ d+)?) \\ s*(?:seconds?|secs?|sec|s) \\ b " ,
1026+ // Duration patterns for timers (with word number support)
1027+ " \\ b( \\ d+(?: \\ . \\ d+)?|one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve|thirteen|fourteen|fifteen|sixteen|seventeen|eighteen|nineteen|twenty|thirty|forty|fifty|sixty|seventy|eighty|ninety|a|an|half|quarter ) \\ s*(?:hours?|hrs?|hr|h) \\ b " ,
1028+ " \\ b( \\ d+(?: \\ . \\ d+)?|one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve|thirteen|fourteen|fifteen|sixteen|seventeen|eighteen|nineteen|twenty|thirty|forty|fifty|sixty|seventy|eighty|ninety|a|an|half|quarter ) \\ s*(?:minutes?|mins?|min|m) \\ b " ,
1029+ " \\ b( \\ d+(?: \\ . \\ d+)?|one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve|thirteen|fourteen|fifteen|sixteen|seventeen|eighteen|nineteen|twenty|thirty|forty|fifty|sixty|seventy|eighty|ninety|a|an|half|quarter ) \\ s*(?:seconds?|secs?|sec|s) \\ b " ,
9851030
9861031 // Combined time patterns
9871032 " \\ b( \\ d+) \\ s*(?:h|hr|hours?) \\ s*(?:and \\ s+)?( \\ d+) \\ s*(?:m|min|minutes?) \\ b " ,
@@ -992,19 +1037,19 @@ class SlotExtractor {
9921037 // Duration keywords
9931038 " \\ b(?:for|during|lasting|takes?) \\ s+( \\ d+(?: \\ . \\ d+)?) \\ s*(?:min|mins|minute|minutes|hours?|hrs?|seconds?|secs?) \\ b " ,
9941039
995- // Timer-specific patterns
996- " \\ b(?:set|start|begin|run|timer|stopwatch) \\ s*(?:for|to|at)? \\ s*( \\ d+(?: \\ . \\ d+)?) \\ s*(?:min|mins|minute|minutes|hours?|hrs?|seconds?|secs?) \\ b " ,
1040+ // Timer-specific patterns (with word number support)
1041+ " \\ b(?:set|start|begin|run|timer|stopwatch) \\ s*(?:for|to|at)? \\ s*( \\ d+(?: \\ . \\ d+)?|one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve|thirteen|fourteen|fifteen|sixteen|seventeen|eighteen|nineteen|twenty|thirty|forty|fifty|sixty|seventy|eighty|ninety|a|an|half|quarter ) \\ s*(?:min|mins|minute|minutes|hours?|hrs?|seconds?|secs?) \\ b " ,
9971042
9981043 // Alarm-specific patterns - capture full time with AM/PM (prioritize space-separated)
9991044 " \\ b(?:alarm|wake|remind|alert) \\ s*(?:at|for|in)? \\ s*( \\ d{1,2}[ \\ s.]+? \\ d{1,2}[ \\ . \\ s]*([ap])(?: \\ .? \\ s*m(?: \\ .?)?)?) \\ b " ,
10001045 " \\ b(?:alarm|wake|remind|alert) \\ s*(?:at|for|in)? \\ s*( \\ d{1,2}(?:: \\ d{2})?[ \\ . \\ s]*([ap])(?: \\ .? \\ s*m(?: \\ .?)?)?) \\ b " ,
10011046 " \\ b(?:alarm|wake|remind|alert) \\ s*(?:at|for|in)? \\ s*( \\ d{3,4}[ \\ . \\ s]*([ap])(?: \\ .? \\ s*m(?: \\ .?)?)?) \\ b " ,
10021047
1003- // "In X time" patterns
1004- " \\ b(?:in|after|within) \\ s+( \\ d+(?: \\ . \\ d+)?) \\ s*(?:min|mins|minute|minutes|hours?|hrs?|seconds?|secs?) \\ b " ,
1048+ // "In X time" patterns (with word number support)
1049+ " \\ b(?:in|after|within) \\ s+( \\ d+(?: \\ . \\ d+)?|one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve|thirteen|fourteen|fifteen|sixteen|seventeen|eighteen|nineteen|twenty|thirty|forty|fifty|sixty|seventy|eighty|ninety|a|an|half|quarter ) \\ s*(?:min|mins|minute|minutes|hours?|hrs?|seconds?|secs?) \\ b " ,
10051050
1006- // Countdown patterns
1007- " \\ b(?:countdown|count \\ s+down) \\ s*(?:from|for)? \\ s*( \\ d+(?: \\ . \\ d+)?) \\ s*(?:min|mins|minute|minutes|hours?|hrs?|seconds?|secs?) \\ b " ,
1051+ // Countdown patterns (with word number support)
1052+ " \\ b(?:countdown|count \\ s+down) \\ s*(?:from|for)? \\ s*( \\ d+(?: \\ . \\ d+)?|one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve|thirteen|fourteen|fifteen|sixteen|seventeen|eighteen|nineteen|twenty|thirty|forty|fifty|sixty|seventy|eighty|ninety|a|an|half|quarter ) \\ s*(?:min|mins|minute|minutes|hours?|hrs?|seconds?|secs?) \\ b " ,
10081053
10091054 // Direct time patterns for alarm setting - capture full time with AM/PM (prioritize space-separated)
10101055 " \\ b(?:set|create|make).*?(?:alarm|wake).*?(?:for|at) \\ s*( \\ d{1,2}[ \\ s.]+? \\ d{1,2}[ \\ . \\ s]*([ap])(?: \\ .? \\ s*m(?: \\ .?)?)?) \\ b " ,
0 commit comments