Repository: hedyorg/hedy

Issue:
Suggestion for misspelling commands prefers echo above ask command level-1
Nice error message when typing askquestion without space:

![image](https://user-images.githubusercontent.com/37830964/108330595-0ec62380-71ce-11eb-9899-7f6b224ec9fa.png)

But then when typing askechoquestion without space:

![image](https://user-images.githubusercontent.com/37830964/108330774-4df47480-71ce-11eb-8934-7a3cff30a331.png)

Error message suggestion preference is the command echo.

I expect a suggestion for the ask command, because the command line starts with ask command?

And perhaps also give a suggestion for the echo command?



Discussion:
We now give suggestions by a very simple string distance algorithm, see https://github.com/Felienne/hedy/blob/master/hedy.py lines 9-32. 

For now it is fine I think, the goal is to be better than "command not found" 

But if you are interested to improve it, as the comment also says "could be more sophisticated MACHINE LEARNING!" :)

Feel free to improve!
Here is my improvement (I do not have access to make branches).

We can in future introduce a phonetic compare function. 

In that case spelling errors children make can be better dealt with. 

![image](https://user-images.githubusercontent.com/37830964/108557254-9b6d0080-72f8-11eb-9279-3fe908634e14.png)


Resolving patch:
diff --git a/hedy.py b/hedy.py
--- a/hedy.py
+++ b/hedy.py
@@ -6,7 +6,10 @@
 
 reserved_words = ['and','except','lambda','with','as','finally','nonlocal','while','assert','false','None','yield','break','for','not','class','from','or','continue','global','pass','def','if','raise','del','import','return','elif','in','True','else','is','try']
 
-# commands are used to suggest the right command when kids make a mistake
+#
+# Commands per Hedy level which are used to suggest the closest command when kids make a mistake
+#
+
 commands_per_level = {1: ['print', 'ask', 'echo'] ,
                       2: ['print', 'ask', 'echo', 'is'],
                       3: ['print', 'ask', 'is'],
@@ -15,8 +18,35 @@
                       6: ['print', 'ask', 'is', 'if', 'repeat']
                       }
 
+# 
+#  closest_command() searches for known commands in an invalid command.
+#
+#  It will return the known command which is closest positioned at the beginning.
+#  It will return '' if the invalid command does not contain any known command. 
+#
+
+def closest_command(invalid_command, known_commands):
+    
+    # First search for 100% match of known commands
+    min_position = len(invalid_command)
+    min_command = ''
+    for known_command in known_commands:
+        position = invalid_command.find(known_command)
+        if position != -1 and position < min_position:
+            min_position = position
+            min_command = known_command
+            
+    # If not found, search for partial match of know commands
+    if min_command == '':
+        min_command = closest_command_with_min_distance(invalid_command, known_commands)
+    
+    return min_command
+
+#
+#  closest_command_with_min_distance()
+#
 
-def closest_command(command, commands):
+def closest_command_with_min_distance(command, commands):
     #simple string distance, could be more sophisticated MACHINE LEARNING!
     min = 1000
     min_command = ''
@@ -813,4 +843,4 @@ def execute(input_string, level):
 
 # f = open('output.py', 'w+')
 # f.write(python)
-# f.close()
\ No newline at end of file
+# f.close()


Test patch:
diff --git a/tests.py b/tests.py
--- a/tests.py
+++ b/tests.py
@@ -4,8 +4,11 @@
 import io
 from contextlib import contextmanager
 
-#this code let's us capture std out to also execute the generated Python
+#
+# This code let's us capture std out to also execute the generated Python
 # and check its output
+#
+
 @contextmanager
 def captured_output():
     new_out, new_err = io.StringIO(), io.StringIO()
@@ -21,6 +24,60 @@ def run_code(code):
         exec(code)
     return out.getvalue().strip()
 
+#
+# Tests for helper functions
+#
+
+class TestsHelperFunctions(unittest.TestCase):
+    
+    def test_closest_command(self):
+        invalid_command = ""
+        closest = hedy.closest_command(invalid_command, ['ask', 'print', 'echo'])
+        self.assertEqual(closest, 'ask')
+
+        invalid_command = "print"
+        closest = hedy.closest_command(invalid_command, ['print', 'ask', 'echo'])
+        self.assertEqual(closest, "print")
+
+        invalid_command = "ask"
+        closest = hedy.closest_command(invalid_command, ['print', 'ask', 'echo'])
+        self.assertEqual(closest, "ask")
+
+        invalid_command = "echo"
+        closest = hedy.closest_command(invalid_command, ['print', 'ask', 'echo'])
+        self.assertEqual(closest, "echo")
+
+        invalid_command = "printechoask"
+        closest = hedy.closest_command(invalid_command, ['print', 'ask', 'echo'])
+        self.assertEqual(closest, "print")
+
+        invalid_command = "printaskecho"
+        closest = hedy.closest_command(invalid_command, ['print', 'ask', 'echo'])
+        self.assertEqual(closest, "print")
+
+        invalid_command = "echoprintask"
+        closest = hedy.closest_command(invalid_command, ['print', 'ask', 'echo'])
+        self.assertEqual(closest, "echo")
+
+        invalid_command = "echoprint ask"
+        closest = hedy.closest_command(invalid_command, ['print', 'ask', 'echo'])
+        self.assertEqual(closest, "echo")
+
+        invalid_command = "echo print ask"
+        closest = hedy.closest_command(invalid_command, ['ask', 'print', 'echo'])
+        self.assertEqual(closest, "echo")
+
+        invalid_command = "hello ask echoprint"
+        closest = hedy.closest_command(invalid_command, ['ask', 'print', 'echo'])
+        self.assertEqual(closest, "ask")
+
+        invalid_command = "hello Ask echo0 print"
+        closest = hedy.closest_command(invalid_command, ['ask', 'print', 'echo'])
+        self.assertEqual(closest, "echo")
+
+#
+#
+#
 
 class TestsForMultipleLevels(unittest.TestCase):
     max_level = 7
@@ -75,14 +132,11 @@ def test_transpile_incomplete(self):
             result = hedy.transpile("print", 1)
         self.assertEqual(str(context.exception), 'Incomplete')
 
-
-
     def test_transpile_incomplete_with_multiple_lines(self):
         with self.assertRaises(Exception) as context:
             result = hedy.transpile("print hallo allemaal\nprint", 1)
         self.assertEqual(str(context.exception), 'Incomplete')
 
-
     def test_transpile_other(self):
         with self.assertRaises(Exception) as context:
             result = hedy.transpile("abc felienne 123", 1)