challenge 制限時間内のキー入力検査

ユーザーの入力を検査する関数 InputCheckerを作成して 3回連続実行してください。

関数 InputCheckerは、以下の仕様を満たしてください。

  1. 引数に 時間(秒) Nと文字列 Sを受け取ること。
  2. ユーザからの標準入力とあらかじめ指定された S を比較し、 一致すれば「OK」、一致しなければ「NG」を標準出力へ出力すること。
  3. ユーザーの入力がN秒以内に完了しなかった場合は、比較せず 「TIME OUT」を標準出力へ出力すること。
  4. 経過時間はユーザーが入力を開始した地点から計測すること。
  5. ENTER キーの入力によってユーザー入力が完了したと仮定すること。
  6. ユーザの入力が完了するまでは、完了を待ち続けること

たとえば、「InputCheker(5, "ABCDEF")」と指定した場合、 出力例はこんな感じです。

  1. input(ABCDEF) =>ABCDEF<ENTER>
    1. result => OK
    2. result => NG
    3. result => TIME OUT

1. input(ABCDEF) =>

と出力して入力待ちをし、ユーザーが「ABCDEF<ENTER>」を入力したとき、 入力開始から5秒以内ならば「OK」、5秒をこえていれば「TIME OUT」を出力します。 このとき、ユーザーがキーを押下しなければ1. を出力してから たとえ10秒たっていても「TIME OUT」にはならないので注意してください。 時間計測はあくまでユーザーが入力を開始してからです。


このお題はraynstardさんの投稿です。ご協力ありがとうございます。

Posted feedbacks - Nested

Flatten Hidden
curses初めて使いました。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
require 'curses'
require 'timeout'

def inputChecker(n, str)
  Curses.init_screen
  Curses.setpos(0, 0)
  Curses.addstr("input(#{str}) => ")
  Curses.refresh

  result = nil

  begin
    input = ""
    c = Curses.getch.chr

    timeout(n){
      until c == "\n"
        input += c
        c = Curses.getch.chr
      end

      result = input == str ? "OK" : "NG"
    }
  rescue Timeout::Error
    result = "TIME OUT"
  end

  Curses.setpos(1, 0)
  Curses.addstr("result => #{result}")
  Curses.refresh
end

inputChecker(5, "ABCDEF")
僕も6番思いっきりシカトでした。
3回連続実行も。これで大丈夫と思うのですが。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
require 'curses'

def inputChecker(n, str)
  Curses.addstr("input(#{str}) => ")
  Curses.refresh

  input = ""
  timeout = false

  until (c = Curses.getch.chr) == "\n"
    timer ||= Thread.new{ sleep(n); timeout = true }
    input += c
  end

  result = timeout ? "TIME OUT" : input == str ? "OK" : "NG"

  Curses.setpos(Curses.stdscr.cury+1, 0)
  Curses.addstr("result => #{result}")
  Curses.refresh

  Curses.setpos(Curses.stdscr.cury+2, 0)
end

if __FILE__ == $0
  Curses.init_screen
  3.times{ inputChecker(5, "ABCDEF") }
  Curses.close_screen
end
Squeak Smalltalk で。

残念ながら Squeak Smalltalk には自身の標準入力やホスト OS のそれとの連携といった概念が(少なくともデフォルト状態では…)ないので、マウスポインタ付近にプロンプトを表示して入力を促すことで対応させていただきました。標準入出力のしかたを問うことが題意でしたら、どうぞあしからず。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
| inputChecker |
inputChecker := [:timeLimitSec :expecStr |
	| char stream start resultMsg initPosition |
	stream := WriteStream with: 'input(', expecStr, ') => '.
	initPosition := stream position.
	start := nil.
	[   stream contents, ' ' displayAt: Sensor cursorPoint + 10 asPoint.
		[Sensor keyboardPressed] whileFalse.
		(char := Sensor keyboard) = Character cr
	] whileFalse: [
		start ifNil: [start := Time millisecondClockValue].
		char == Character backspace
			ifTrue: [stream position: (stream position - 1 max: initPosition)]
			ifFalse: [stream nextPut: char]].
	resultMsg := (Time millisecondClockValue - start) / 1.0e3 <= timeLimitSec
		ifFalse: ['TIME OUT']
		ifTrue: [(stream contents last: expecStr size) = expecStr
			ifTrue: ['OK'] ifFalse: ['NG']].
	#result -> resultMsg].

inputChecker value: 5 value: 'ABCDEF'
標準ではない System.Timeout モジュールを使う
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
module Main (main) where

import System.Environment
import System.IO
import System.Timeout

main :: IO ()
main = do { prog <- getProgName
          ; args <- getArgs
          ; case args of
              t:str:_ -> loop 3 (inputChecker (read t) str)
              _       -> usage prog
          }

loop :: Int -> IO () -> IO ()
loop 0 _ = return ()
loop n f = f >> loop (n-1) f

usage :: String -> IO ()
usage prog = hPutStrLn stderr $ prog ++ " <timeout> <string>"

inputChecker :: Int -> String -> IO ()
inputChecker t s
 = do { hPutStr stderr $ "input(" ++ s ++ ") => "
      ; hSetBuffering stdin NoBuffering
      ; c <- hGetChar stdin
      ; ms <- timeout (t*1000000) (hGetLine stdin)
      ; case ms of
          Nothing -> hPutStrLn stdout "\n result => TIME OUT" >> hFlush stdout
          Just s' -> if s == (c:s') then hPutStrLn stdout " result => OK" >> hFlush stdout
                     else hPutStrLn stdout " result => NG" >> hFlush stdout
      }

{-
*Main> :main 5 ABCDEF
input(ABCDEF) => ABCDEF
 result => OK
input(ABCDEF) => A
 result => TIME OUT
input(ABCDEF) => PQR
 result => NG
*Main> _
-}
補足です。
System.Timeoutモジュールは、次のghcのバージョン6.8.xのbaseパッケージに追加されるようです。
ああーっ。仕様6を満たしてなかった。
timeout というので短絡的に仕様6が頭から
飛んじゃったみたい。
仕様6があるほうがやさしいかも。。。

というわけで変更。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import System.Environment
import System.IO
import System.Time

main :: IO ()
main = do { prog <- getProgName
          ; args <- getArgs
          ; case args of
              t:str:_ -> loop 3 (inputChecker (read t) str)
              _       -> usage prog
          }

loop :: Int -> IO () -> IO ()
loop 0 _ = return ()
loop n f = f >> loop (n-1) f

usage :: String -> IO ()
usage prog = hPutStrLn stderr $ prog ++ " <timeout> <string>"

inputChecker :: Int -> String -> IO ()
inputChecker t s
 = do { hPutStr stderr $ "input(" ++ s ++ ") => "
      ; hSetBuffering stdin NoBuffering
      ; c <- hGetChar stdin
      ; start <- getClockTime
      ; s' <- hGetLine stdin
      ; end <- getClockTime
      ; let interval = diffClockTimes end start
      ; if interval > noTimeDiff {tdSec = t}
          then hPutStrLn stdout " result => TIME OUT" >> hFlush stdout
          else if s == (c:s') 
                 then hPutStrLn stdout " result => OK" >> hFlush stdout
                 else hPutStrLn stdout " result => NG" >> hFlush stdout
      }

{-
*Main> :main 5 ABCDEF
input(ABCDEF) => ABCDEF
 result => OK
input(ABCDEF) => ABC
 result => NG
input(ABCDEF) => ABCDEF
 result => TIME OUT
-}
これも標準入出力ではありません。ブラウザ上でキーイベントを受けて、ブラウザ上に結果を表示します。
標準入出力を使うことが必須の要件でしたらゴメンナサイです。
IEとFirefoxのみで確認してます。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
function InputChecker(timeout, s) {
  var start;
  var input = [];
  var prompt = "input("+ s + ")=&gt;"
  var pre = document.createElement("pre");
  pre.innerHTML = prompt;
  document.body.appendChild(pre);
  
  function result() {
    var time = new Date().getTime() - start.getTime();
    if(time > timeout * 1000) return "TIMEOUT";
    else if(s == input) return "OK";
    else return "NG"
  }
  var f = function(e) {
    e = e || event;
    if(!start) start = new Date();
    if(e.keyCode == 13) { // Enter
      pre.innerHTML += "<br/>result=&gt;" + result() + "<br/>" + prompt;
      input = "";
      start = undefined;
    } else if(e.charCode == 0 && e.keyCode) { // for firefox. ignore BS, Del, .. etc.
    } else {
      var c = String.fromCharCode(e.keyCode || e.charCode);
      pre.innerHTML += c;
      input += c;
    }
  }
  if(document.attachEvent) document.attachEvent("onkeypress", f);
  else if(document.addEventListener) document.addEventListener("keypress", f, false);
  else document.onkeypress = f;
}
new InputChecker(5, "ABCDEF");
<form>を使えばHTMLでもいけるかなと思っていたのですが、まさかそのままで実現するとは思いつきませんでした~。 やってみてびっくり、まるでコンソールのように動きますね(笑
ども、raynstardです。
あくまで管理人さんの意志を優先しますが
僕がお題を投稿したときの意図としては
「時間計測はあくまでユーザーが入力を開始してからです。」を
どう実現するかを気にしていました。

なのでキー入力をトリガにして
非同期的な時間計測が実現されていれば
良いかなーなんて考えていました。

お題投稿した時点で、getche()を使えるのが
C,python,RUBYくらいしか見つからなかったので。。。
ちょっと冗漫ですが、こんなところで。

# try節からreturnしてもfinally通るんだっけかな。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import termios, sys, tty, select

def InputChecker(N, S):
  sys.stdout.write('input(%s) => ' % S)
  sys.stdout.flush()
  fd = sys.stdin.fileno()
  poll = select.poll()
  poll.register(fd, select.POLLIN)
  attr = termios.tcgetattr(fd)
  tty.setcbreak(fd)
  s = ''
  flg = False
  try:
    while True:
      t = poll.poll(N * 1000)
      if not t:
        if not flg:
          continue
        print '\nresult => TIME OUT'
        break
      flg = True
      c = sys.stdin.read(1)
      sys.stdout.write(c)
      sys.stdout.flush()
      if c == '\n':
        if S == s:
          print 'result => OK'
        else:
          print 'result => NG'
        break
      s += c
  finally:
    termios.tcsetattr(fd, termios.TCSADRAIN, attr)
    poll.unregister(fd)

InputChecker(5, 'ABCDEF')
少しだけ改良。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
from sys import stdin, stdout
import tty, select

def write(s):
  stdout.write(s)
  stdout.flush()

def InputChecker(N, S):
  write('input(%s) => ' % S)
  poll = select.poll()
  poll.register(stdin, select.POLLIN)
  attr = tty.tcgetattr(stdin)
  tty.setcbreak(stdin)
  s = ''
  try:
    while True:
      l = poll.poll(N * 1000)
      if not l:
        if not s:
          continue
        write('\nresult => TIME OUT\n')
        return
      c = stdin.read(1)
      write(c)
      if c == '\n':
        write('result => %s\n' % ('OK' if S == s else 'NG'))
        return
      s += c
  finally:
    tty.tcsetattr(stdin, tty.TCSADRAIN, attr)
    poll.unregister(stdin)

InputChecker(5, 'ABCDEF')
条件6の対応もれを訂正。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
from sys import stdin, stdout
import tty, select

def write(s):
  stdout.write(s)
  stdout.flush()

def InputChecker(N, S):
  write('input(%s) => ' % S)
  poll = select.poll()
  poll.register(stdin, select.POLLIN)
  attr = tty.tcgetattr(stdin)
  tty.setcbreak(stdin)
  s = ''
  timeout = False
  try:
    while True:
      if not poll.poll(N * 1000):
        if not s:
          continue
        timeout = True
      c = stdin.read(1)
      write(c)
      if c == '\n':
        write('result => %s\n' % ('TIME OUT' if timeout else 'OK' if S == s else 'NG'))
        return
      s += c
  finally:
    tty.tcsetattr(stdin, tty.TCSADRAIN, attr)
    poll.unregister(stdin)

InputChecker(5, 'ABCDEF')
># try節からreturnしてもfinally通るんだっけかな。

通ります。

>>> def foo():
	try:
		return
	finally:
		print 1

		
>>> foo()
1
c-wrapperでcursesを使いました。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
(use c-wrapper)
(c-load "curses.h" :libs "-lcurses")

(initscr)

(define (input-checker n str)
  (cbreak)
  (printw "input(%s) => " str)
  (refresh)
  (receive (t input) (let* ((c0 (integer->char (getch)))
                            (st (sys-time)))
                       (do ((c c0 (integer->char (getch)))
                            (lst '() (cons c lst)))
                           ((memq c '(#\lf #\cr))
                            (values (- (sys-time) st) (list->string (reverse lst))))))
    (printw "result => %s\n" (cond
                              ((< n t) "TIME OUT")
                              ((equal? str input) "OK")
                              (else "NG")))
    (refresh)))

(input-checker 5 "ABCDEF")
すみません、さっきのコードでmemqを使っている箇所がありますが、文字の判定なので正しくはmemvを使うべきですね。

Gauche 0.8.11だとmemqでも動くようですが。
手元で試してみたら最後に(endwin)の
終了処理を入れないと、
モードが元にもどらないでエスケープ絡みがおかしくなるようです。

しかしc-wrapperってすごいですね。
たしかにmanで見てみると、"A program should always call endwin before exiting or escaping from curses mode temporarily." とあるので、endwinを呼ぶべきですね。行儀が悪いプログラムでした。
Emacs のミニバッファから。
1
2
3
4
5
6
7
(defun input-checker (n s)
  (interactive "nTime(second): \nsString: ")
  (let ((c (read-char (concat "input(" s ")=>"))))
    (push c unread-command-events))
  (with-timeout (n (message "TIME OUT"))
    (let ((input (read-string (concat "input(" s ")=>"))))
      (message (if (string= input s) "OK" "NG")))))
秒数はあまり正確でないです。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
using System;
using System.Threading;
class Program {
    static void InputCheck(int n, string s) {
        Console.Write("input({0}) => ", s);
        while (!Console.KeyAvailable) Thread.Sleep(100);
        DateTime timeout = DateTime.Now.AddSeconds(n); 
        string match = s.Equals(Console.ReadLine()) ? "OK" : "NG";
        Console.WriteLine("result => {0}", DateTime.Now < timeout ? match : "TIME OUT");
    }
    static void Main(string[] args) {
        InputCheck(5, "ABCDEFG");
        InputCheck(4, "HIJKLMN");
        InputCheck(3, "OPQRSTUVWXYZ");
    }
}
ReadLine()は使わないで書いてあるようなのですが、
平気なのでしょうか?

by C#わからない人
KeyAvailableがtrueでも、ReadLine, Readは、Enterキー待ちになるので組み合わせるな、と解釈しましたが…
他の理由で組み合わせるなと書いてる可能性も、無いとはいえないですね。

ReadKeyを使ったのも書いて比べてみました。
動作の違いは、ReadLineは整形済みの文字列の入力であるのに対し、ReadKeyは、コード単位の入力になります。
具体的には、バックスペースが'\b'として入力される、日本語入力には、対応するエンコーディングの変換が必要、など。
なので、このコードはASCIIコードのみ対応で、打ち間違いは許されません。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
static void InputChecker(int n, string s) {
    StringBuilder sb = new StringBuilder(s.Length * 2);
    Console.Write("input({0}) => ", s);
    ConsoleKeyInfo key = Console.ReadKey();
    DateTime timeout = DateTime.Now.AddSeconds(n);
    while (key.Key != ConsoleKey.Enter) {
        sb.Append(key.KeyChar);
        key = Console.ReadKey();
    }
    string match = s.Equals(sb.ToString()) ? "OK" : "NG";
    Console.WriteLine();
    Console.WriteLine("result => {0}", DateTime.Now < timeout ? match : "TIME OUT");
}
難しいですね……。なんでダメなんだろ。少し調べてみました。
ReadKeyとKeyAvailableは、それぞれWindows APIのReadConsoleInputとPeekConsoleInputを内部で呼び出しています。
これらの関数の引数にはコンソール入力バッファのハンドルを必要としていて
そのハンドルをWindows APIのGetStdHandleで取得し使用しています。
ReadKeyとKeyAvailableではこのハンドルを共有して(Private変数で)いるのに対し
ReadLineではメソッドが呼ばれたときGetStdHandleを呼び出し新たにハンドルを取得して
それを元にSystem.IO.Streamオブジェクトを作成しています。
ハンドルの値は結果的に同じになるのでしょうが、ここらあたりが原因なのかな?

そしてVB.NETのコードも併せて。ReadLineの方で……。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
Sub Main()
    InputCheck(5, "ABCDEFG")
    InputCheck(4, "HIJKLMN")
    InputCheck(3, "OPQRSTUVWXYZ")
End Sub

Sub InputCheck(ByVal n As Integer, ByVal s As String)
    Console.Write("input({0}) => ", s)
    Do While Not Console.KeyAvailable
        Threading.Thread.Sleep(100)
    Loop
    Dim time As DateTime = Now
    Dim input As String = Console.ReadLine
    Dim a As Double = Now.Subtract(time).TotalMilliseconds
    If Now.Subtract(time).TotalMilliseconds > 1000 * n Then
        Console.WriteLine("TIMEOUT")
    ElseIf input.Equals(s) Then
        Console.WriteLine("OK")
    Else
        Console.WriteLine("NG")
    End If
End Sub
Windows専用。
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
#include <windows.h>
#include <stdexcept>
#include <string>

char GetChar(HANDLE h)
{
    char c;

    DWORD read;

    while (true)
    {
        if (! ::ReadFile(h, &c, 1, &read, NULL))
        {
            throw std::runtime_error("::ReadFile() failed");
        }

        if (read == 1 && c != '\r') // skip CR
        {
            break;
        }
    }

    return c;
}

void WriteString(HANDLE h, const std::string& s)
{
    const char* p = s.c_str();

    DWORD size = s.size();

    while (size)
    {
        DWORD written;

        if (! ::WriteFile(h, p, size, &written, NULL))
        {
            throw std::runtime_error("::WriteFile() failed");
        }

        p += written;

        size -= written;
    }

    ::FlushFileBuffers(h);
}

void InputChecker(DWORD timeout, const std::string& expected)
{
    HANDLE hStdIn = ::GetStdHandle(STD_INPUT_HANDLE);

    HANDLE hStdOut = ::GetStdHandle(STD_OUTPUT_HANDLE);

    if (hStdIn == INVALID_HANDLE_VALUE || hStdOut == INVALID_HANDLE_VALUE)
    {
        throw std::runtime_error("cannot get console handle");
    }

    WriteString(hStdOut, "input(" + expected + ") => ");

    ::WaitForSingleObject(hStdIn, INFINITE);

    const DWORD beg = ::GetTickCount();

    std::string s;

    char c;

    while ((c = GetChar(hStdIn)) != '\n')
    {
        s.append(1, c);
    }

    const DWORD end = ::GetTickCount();

    std::string result;

    if (end - beg >= timeout * 1000)
    {
        result = "TIME OUT";
    }
    else if (s == expected)
    {
        result = "OK";
    }
    else
    {
        result = "NG";
    }

    WriteString(hStdOut, "   result => " + result + "\r\n");
}

int main()
{
    try
    {
        while (true)
        {
            InputChecker(5, "ABCDEF");
        }
    }
    catch (std::exception& e)
    {
        WriteString(::GetStdHandle(STD_ERROR_HANDLE), std::string("error: ") + e.what() + "\r\n");
    }
}
3回実行というのを見落としてました。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
--- orig.cpp	Wed Oct  3 04:14:39 2007
+++ main.cpp	Wed Oct  3 04:15:00 2007
@@ -97,7 +97,7 @@
 {
     try
     {
-        while (true)
+        for (int i = 0; i < 3; ++i)
         {
             InputChecker(5, "ABCDEF");
         }
真っ先に投稿があると思っていたCがなかなかこないので投稿(笑

環境がcygwinのせいかcursesのライブラリがうまく動かず、
それならばと int 16hしてみたがやっぱりダメ;;

ということで、termiosを使いました。
あといらんコード混ぜて見た目ちょっとだけかっこよくしてみた(笑

//gcc -Wall -std=c99 doukaku64.c -o test
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#include <stdio.h>
#include <string.h>
#include <time.h>
#include <sys/syslimits.h>
#include <unistd.h>
#include <termios.h>

int InputChecker(time_t sec, const char *str)
{
    size_t length = strlen(str);    // オリジナルの文字数
    char in[ LINE_MAX+1 ];          // 入力文字列
    size_t len;                     // 入力文字数
    time_t t;                       // 入力時間
    struct termios tos, tos_org;

    tcgetattr( 0, &tos ); tos_org = tos;

    memset(in, '\0', sizeof(in));
    printf("input:[%s] => 入力待ち\r", str); fflush(stdout);
    printf("input:[%s] => ", str);