├── .gitignore ├── ExerciseGeneratorApp.iss ├── ExerciseGeneratorApp.py ├── ExerciseGeneratorDlg.py ├── ExerciseGeneratorDlg.wxg ├── README.md ├── app_dist ├── exercise_generator_setup_v1.3.exe ├── exercise_generator_setup_v1.4.exe └── exercise_generator_setup_v1.5.exe ├── build_exercise_generator_app.bat ├── clock ├── 1.png ├── 2.png ├── 3.png ├── 4.png ├── 5.png ├── 6.png └── run.py ├── genxword ├── __init__.py ├── calculate.py ├── complexstring.py ├── complexstring2.py └── control.py ├── msvcp90.dll ├── pyperclip ├── __init__.py ├── clipboards.py ├── exceptions.py └── windows.py ├── setup_ExerciseGeneratorApp.py ├── snapshots ├── demo.png ├── win7.png ├── winxp.png └── xubuntu.png └── sudokulib ├── __init__.py ├── decorators.py ├── jigsaw.py └── sudoku.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *~ 3 | *.swp 4 | *.xls 5 | *.xlsx 6 | 7 | build/ 8 | dist/ 9 | 10 | 11 | -------------------------------------------------------------------------------- /ExerciseGeneratorApp.iss: -------------------------------------------------------------------------------- 1 | ; 2 | 3 | [Setup] 4 | AppName=自动出题机 5 | AppVersion=1.5 6 | DefaultDirName={pf}\ExerciseGenerator 7 | DefaultGroupName=自动出题机 8 | DisableProgramGroupPage=yes 9 | Compression=lzma2 10 | SolidCompression=yes 11 | OutputDir=app_dist 12 | OutputBaseFilename=exercise_generator_setup_v1.5 13 | 14 | [Files] 15 | Source: "dist\*"; DestDir: "{app}" 16 | 17 | [Icons] 18 | Name: "{group}\自动出题机"; Filename: "{app}\ExerciseGeneratorApp.exe" 19 | Name: "{group}\Uninstall"; Filename: "{uninstallexe}" 20 | -------------------------------------------------------------------------------- /ExerciseGeneratorApp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: UTF-8 -*- 3 | # 4 | from __future__ import unicode_literals 5 | import os 6 | import re 7 | import sys 8 | import math 9 | import time 10 | import random 11 | import keyword 12 | from math import * 13 | from random import * 14 | from ExerciseGeneratorDlg import * 15 | #from sudoku_maker import SudokuMaker 16 | import sudokulib 17 | import sudokulib.sudoku 18 | from sudokulib.sudoku import Sudoku 19 | import pypinyin 20 | from pypinyin import pinyin, lazy_pinyin 21 | from genxword.control import Genxword 22 | from genxword.calculate import Crossword 23 | import traceback 24 | 25 | 26 | ABOUT_INFO = '''\ 27 | Python自动出题程序 V1.5 28 | 将生成结果复制粘帖到Excel/WPS中排版 29 | 30 | 规则说明: 31 | 1. 定义必须包含generator函数,其返回值必须为字符串列表,作为单次出题结果。 32 | 2. 用ASSERT函数筛除不符合规则的出题。 33 | 3. 用STOP函数结束出题循环。 34 | 4. math/random库的所有函数已导入,可直接使用。 35 | 6. 支持pypinyin库,可直接使用pinyin,lazy_pinyin函数。 36 | 6. 支持unicode字符串。 37 | 7. 当返回结果项为字符串“EOL”时,换行输出。 38 | 8. 用load_cn_words函数从UTF8编码的TXT文件加载词语,词语间用空格或换行分隔,忽略#开头的注释部分,忽略~开头的词语。 39 | 9. 用load_en_words函数加载单词表,每行单词先英文后中文,用至少三个空格分隔。 40 | 41 | 42 | URL: https://github.com/pengshulin/exercise_generator 43 | Peng Shullin 2019 44 | ''' 45 | 46 | CONFIGS_LIST = [ 47 | ['生成随机数', '''\ 48 | def generator(): 49 | MIN, MAX = 0, 100 50 | a = randint(MIN, MAX) 51 | return [ a ] 52 | '''], 53 | 54 | ['生成整行随机数', '''\ 55 | def generator(): 56 | l, LINE_LIMIT = '', 50 57 | while len(l) < LINE_LIMIT: 58 | #l = l + str(randint(0, 9)) 59 | #l = l + str(randint(0, 9)) + ' ' 60 | l = l + str(randint(10, 99)) + ' ' 61 | #l = l + str(randint(100, 999)) + ' ' 62 | return [ l ] 63 | '''], 64 | 65 | 66 | ['加减法(2个数)', '''\ 67 | def generator(): 68 | MIN, MAX = 0, 100 69 | a = randint(MIN, MAX) 70 | b = randint(MIN, MAX) 71 | oper = choice( '+-' ) 72 | if oper == '+': 73 | c = a + b 74 | jinwei = bool( a%10 + b%10 >= 10 ) 75 | #ASSERT( jinwei ) # 进位 76 | #ASSERT( not jinwei ) # 不进位 77 | else: 78 | c = a - b 79 | tuiwei = bool( a%10 < b%10 ) 80 | #ASSERT( tuiwei ) # 退位 81 | #ASSERT( not tuiwei ) # 不退位 82 | ASSERT( 0 <= c <= MAX ) 83 | #return [ a, oper, b, '=', '□' ] 84 | #return [ '%s%s%s='% (a, oper, b) ] 85 | #return [ '%s%s%s='% (a, oper, b), c ] 86 | #return [ '%s%s%s'% (a, oper, b), '=', c ] 87 | #return [ a, oper, '□', '=', c ] 88 | return [ a, oper, b, '=', c ] 89 | '''], 90 | 91 | ['乘法(2个数)', '''\ 92 | def generator(): 93 | MIN, MAX = 1, 10 94 | a = randint(MIN, MAX) 95 | b = randint(MIN, MAX) 96 | c = a * b 97 | ASSERT( 0 <= c <= 100 ) 98 | return [ a, '×', b, '=', c ] 99 | '''], 100 | 101 | ['比较大小(2个数)', '''\ 102 | def generator(): 103 | MIN, MAX = 0, 100 104 | a = randint(MIN, MAX) 105 | b = randint(MIN, MAX) 106 | if a > b: 107 | oper = '>' 108 | elif a < b: 109 | oper = '<' 110 | else: 111 | oper = '=' 112 | #return [ a, '○', b ] 113 | return [ a, oper, b ] 114 | '''], 115 | 116 | ['连加连减(3个数)', '''\ 117 | def generator(): 118 | MIN, MAX = 0, 100 119 | a = randint(MIN, MAX) 120 | b = randint(MIN, MAX) 121 | c = randint(MIN, MAX) 122 | oper1 = choice( '+-' ) 123 | oper2 = choice( '+-' ) 124 | if oper1 == '+': 125 | ab = a + b 126 | else: 127 | ab = a - b 128 | ASSERT( 0 <= ab <= MAX ) 129 | if oper2 == '+': 130 | d = ab + c 131 | else: 132 | d = ab - c 133 | ASSERT( 0 <= d <= MAX ) 134 | #return [ '%s%s%s%s%s=%s'% (a, oper1, b, oper2, c, d) ] 135 | #return [ '%s%s%s%s%s='% (a, oper1, b, oper2, c) ] 136 | return [ a, oper1, b, oper2, c, '=', d ] 137 | '''], 138 | 139 | ['三角分解', '''\ 140 | def generator(): 141 | MIN, MAX = 0, 20 142 | a = randint(MIN, MAX) 143 | b = randint(MIN, MAX) 144 | c = randint(MIN, MAX) 145 | ab = a + b 146 | bc = b + c 147 | ac = a + c 148 | ASSERT( 0 <= ab <= MAX ) 149 | ASSERT( 0 <= bc <= MAX ) 150 | ASSERT( 0 <= ac <= MAX ) 151 | return [ 'EOL','EOL',ab,'',a,'',ac,'EOL','EOL','',b,'',c,'EOL','EOL','','',bc] 152 | '''], 153 | 154 | 155 | ['数墙(3层)', '''\ 156 | def generator(): 157 | MIN, MAX = 0, 20 158 | a = randint(MIN, MAX) 159 | b = randint(MIN, MAX) 160 | c = randint(MIN, MAX) 161 | ab = a + b 162 | bc = b + c 163 | abc = ab + bc 164 | ASSERT( 0 <= ab <= MAX ) 165 | ASSERT( 0 <= bc <= MAX ) 166 | ASSERT( 0 <= abc <= MAX ) 167 | return [ 'EOL','','',abc,'EOL','',ab,'',bc,'EOL',a,'',b,'',c] 168 | '''], 169 | 170 | ['数墙(4层)', '''\ 171 | def generator(): 172 | MIN, MAX = 0, 20 173 | a = randint(MIN, MAX) 174 | b = randint(MIN, MAX) 175 | c = randint(MIN, MAX) 176 | d = randint(MIN, MAX) 177 | ab = a + b 178 | bc = b + c 179 | cd = c + d 180 | abc = ab + bc 181 | bcd = bc + cd 182 | abcd = abc + bcd 183 | ASSERT( 0 <= ab <= MAX ) 184 | ASSERT( 0 <= bc <= MAX ) 185 | ASSERT( 0 <= cd <= MAX ) 186 | ASSERT( 0 <= abc <= MAX ) 187 | ASSERT( 0 <= bcd <= MAX ) 188 | ASSERT( 0 <= abcd <= MAX ) 189 | return [ 'EOL','','','',abcd,'EOL','','',abc,'',bcd,'EOL','',ab,'',bc,'',cd,'EOL',a,'',b,'',c,'',d] 190 | '''], 191 | 192 | ['排序(4个数)', '''\ 193 | def generator(): 194 | MIN, MAX = 0, 100 195 | lst = [randint(MIN, MAX) for i in range(4)] 196 | lst.sort() 197 | a, b, c, d = lst 198 | ASSERT( a < b < c < d ) 199 | #r=[a,b,c,d]; shuffle(r); return r 200 | return [ a, '<', b, '<', c, '<', d ] 201 | '''], 202 | 203 | ['排序(5个数)', '''\ 204 | def generator(): 205 | MIN, MAX = 0, 100 206 | lst = [randint(MIN, MAX) for i in range(5)] 207 | lst.sort() 208 | a, b, c, d, e = lst 209 | ASSERT( a < b < c < d < e ) 210 | r=[a,b,c,d,e]; shuffle(r); s=', '.join(map(str,r)) 211 | return [ s, '>'.join(['____']*5) ] 212 | '''], 213 | 214 | ['算术题排序(5个算术题)', '''\ 215 | def generator(): 216 | MIN, MAX = 0, 20 217 | def generate_single(): 218 | a, b = randint(MIN, MAX), randint(MIN, MAX) 219 | return (a,b,a+b) 220 | 221 | a1, a2, a = generate_single() 222 | ASSERT( MIN <= a <= MAX ) 223 | b1, b2, b = generate_single() 224 | ASSERT( MIN <= b <= MAX ) 225 | ASSERT( b != a ) 226 | c1, c2, c = generate_single() 227 | ASSERT( MIN <= c <= MAX ) 228 | ASSERT( c not in [a,b] ) 229 | d1, d2, d = generate_single() 230 | ASSERT( MIN <= d <= MAX ) 231 | ASSERT( d not in [a,b,c] ) 232 | e1, e2, e = generate_single() 233 | ASSERT( MIN <= e <= MAX ) 234 | ASSERT( e not in [a,b,c,d] ) 235 | 236 | r=[[a1,a2],[b1,b2],[c1,c2],[d1,d2],[e1,e2]] 237 | shuffle(r) 238 | s=', '.join(['%s+%s'% (x,y) for x,y in r]) 239 | return [ s, '>'.join(['________']*5) ] 240 | '''], 241 | 242 | ['凑整数', '''\ 243 | def generator(): 244 | MIN, MAX = 0, 100 245 | a = randint(MIN, MAX) 246 | b = randint(MIN, MAX) 247 | ASSERT( 1 <= b <= 9 ) 248 | #oper = choice( '+-' ) 249 | oper = '+' 250 | if oper == '+': 251 | c = a + b 252 | else: 253 | c = a - b 254 | ASSERT( 10 <= c <= MAX ) 255 | ASSERT( c % 10 == 0 ) 256 | return [ a, oper, b, '=', c ] 257 | '''], 258 | 259 | ['带拼音汉字抄写', '''\ 260 | source = load_cn_words('e:\\\\文档\\\\自动出题\\\\词语默写.txt') 261 | #source=list(''.join(source)) # 紧缩模式 262 | 263 | copies=1 264 | if copies > 1: 265 | source=[i*copies for i in source] # 创建副本 266 | 267 | columns, rows = 13, 11 # 控制每页行列 268 | page_limit = 10 # 控制输出页数 269 | row_cur, page_cur = 1, 1 270 | def generator(): 271 | global source, columns, rows, row_cur, page_cur, page_limit 272 | if not source or page_cur > page_limit: 273 | STOP() 274 | word = source.pop(0)[:columns] 275 | pinyin_lst = [i[0] for i in pinyin(word)] 276 | hanzi_lst = list(word) 277 | space_lst=[' ' for i in range(columns-len(word))] 278 | ret = pinyin_lst + space_lst + ['EOL'] + hanzi_lst + space_lst 279 | row_cur += 1 280 | if row_cur > rows: 281 | row_cur = 1 282 | page_cur += 1 283 | return ret 284 | '''], 285 | 286 | ['看拼音默写词语', '''\ 287 | source = load_cn_words('e:\\\\文档\\\\自动出题\\\\词语默写.txt') 288 | #shuffle(source) # 打乱顺序 289 | 290 | columns, rows = 13, 11 # 控制每页行列 291 | page_limit = 10 # 控制输出页数 292 | row_cur, page_cur = 1, 1 293 | def generator(): 294 | global source, columns, rows, row_cur, page_cur, page_limit 295 | if not source or page_cur > page_limit: 296 | STOP() 297 | pinyin_lst, hanzi_lst = [], [] 298 | while True: 299 | if not source: 300 | break 301 | word = source.pop(0) 302 | if len(pinyin_lst) + len(word) <= columns: 303 | pinyin_lst += [i[0] for i in pinyin(word)] 304 | hanzi_lst += list(word) 305 | if len(pinyin_lst) + 1 < columns: 306 | pinyin_lst.append(' ') 307 | hanzi_lst.append(' ') 308 | else: 309 | source.insert(0, word) 310 | break 311 | while len(pinyin_lst) < columns: 312 | pinyin_lst.append(' ') 313 | hanzi_lst.append(' ') 314 | row_cur += 1 315 | if row_cur > rows: 316 | row_cur = 1 317 | page_cur += 1 318 | # 选择输出内容 319 | #return pinyin_lst + ['EOL'] + hanzi_lst # 拼音+汉字 320 | #return ['EOL'] + hanzi_lst # 仅汉字 321 | return pinyin_lst + ['EOL'] # 仅拼音 322 | '''], 323 | 324 | 325 | ['舒尔特方格', '''\ 326 | size=5 327 | 328 | def generator(): 329 | global size 330 | source = range(1,size**2+1) 331 | shuffle(source) 332 | ret = [] 333 | for i in range(size): 334 | ret += [ '%s'% source.pop() for j in range(size) ] 335 | ret.append('EOL') 336 | return ret 337 | '''], 338 | 339 | 340 | ['人民币计算', '''\ 341 | def generator(): 342 | MIN, MAX = 0.1, 10.0 343 | a = round(random()*MAX, 1) 344 | b = round(random()*MAX, 1) 345 | oper = choice( '+-' ) 346 | if oper == '+': 347 | c = a + b 348 | else: 349 | c = a - b 350 | ASSERT( MIN <= a <= MAX ) 351 | ASSERT( MIN <= b <= MAX ) 352 | ASSERT( MIN <= c <= MAX ) 353 | #return [ a, oper, b, '=', c ] 354 | return [ getRMBName(a), oper, getRMBName(b), '=', '____元____角' ] 355 | '''], 356 | 357 | 358 | 359 | ['数独', '''\ 360 | def generator(): 361 | GRID = 3 362 | ret = [] 363 | board = Sudoku( GRID ) 364 | board.solve() 365 | for i in range(GRID**2): 366 | for j in range(GRID**2): 367 | v = board.masked_grid[i*GRID**2+j] 368 | #v = board.solution[i*GRID**2+j] 369 | ret.append( ' ' if v == '_' else v ) 370 | ret.append('EOL') 371 | return ret 372 | '''], 373 | 374 | 375 | ['加减乘法表', '''\ 376 | oper='*' 377 | line=1 378 | def generator(): 379 | global line, oper 380 | if line > 10: 381 | STOP() 382 | ret = [] 383 | for i in range(line, 10+1): 384 | if oper == '*': 385 | s = '%d%s%d=%d'% (line, oper, i, line * i ) 386 | elif oper == '+': 387 | s = '%d%s%d=%d'% (line, oper, i, line + i ) 388 | elif oper == '-': 389 | s = '%d%s%d=%d'% (i, oper, line, i - line ) 390 | ret.append( s ) 391 | line += 1 392 | return ret 393 | '''], 394 | 395 | 396 | ['12/24时间制式转换', '''\ 397 | def getTimeName(hour, minute, format_24h): 398 | if format_24h: 399 | r = '%d时'% hour 400 | else: 401 | if hour <= 12: 402 | r = '上午%d时'% hour 403 | else: 404 | r = '下午%d时'% (hour-12) 405 | if minute == 0: 406 | pass 407 | elif minute == 30: 408 | r += '半' 409 | return r 410 | 411 | def generator(): 412 | global getTimeName 413 | hour = randint(0,23) 414 | minute = choice([0,30]) 415 | name = '%d:%02d'% (hour, minute) 416 | return [ name, getTimeName(hour,minute,False), getTimeName(hour,minute,True) ] 417 | '''], 418 | 419 | 420 | ['四则混合运算(3个数)', '''\ 421 | def generator(): 422 | MIN, MAX = 0, 10000 423 | a = randint(MIN, 99) 424 | b = randint(MIN, 99) 425 | c = randint(MIN, MAX) 426 | oper1 = choice( '*/' ) 427 | oper2 = choice( '+-' ) 428 | inv = bool(random() > 0.5) 429 | ASSERT( a < 100 ) 430 | ASSERT( b < 100 ) 431 | if oper1 == '*': 432 | ab = a * b 433 | else: 434 | ASSERT( b > 0 ) 435 | ab = a / b 436 | ASSERT( ab * b == a ) 437 | ASSERT( 0 <= ab <= MAX ) 438 | if oper2 == '+': 439 | d = ab + c 440 | elif inv: 441 | d = c - ab 442 | else: 443 | d = ab - c 444 | ASSERT( 0 <= d <= MAX ) 445 | if inv: 446 | return [ c, oper2, a, oper1, b, '=', d ] 447 | else: 448 | return [ a, oper1, b, oper2, c, '=', d ] 449 | '''], 450 | 451 | 452 | ['除法', '''\ 453 | def generator(): 454 | #NO_REMAINING = True # 不允许有余数 455 | NO_REMAINING = False # 允许有余数 456 | MIN, MAX = 1, 100 457 | MIN2, MAX2 = 2, 10 458 | a = randint(MIN, MAX) 459 | b = randint(MIN2, MAX2) 460 | c = a/b 461 | d = a - b * c 462 | ASSERT( c >= 1 ) 463 | #ASSERT( c <= 10 ) # 简单除法 464 | if NO_REMAINING: 465 | ASSERT( d == 0 ) # 没有余数 466 | return [ a, '÷', b, '=', c ] 467 | else: 468 | #ASSERT( d > 0 ) # 余数不能为零 469 | ASSERT( d >= 0 ) # 余数允许为零 470 | return [ a, '÷', b, '=', c, '...', d ] 471 | '''], 472 | 473 | ['英语单词默写', '''\ 474 | # 注:不同文件加载的单词列表可用加号拼接,如:dictionary += load_en_words(第二个单词表文件) 475 | dictionary = load_en_words('e:\\\\文档\\\\自动出题\\\\英语单词表\\\\上海版牛津小学英语单词表\\\\2A.txt') 476 | 477 | RANDOM_MODE = True # 随机选词 478 | RANDOM_MODE = False # 顺序选词 479 | 480 | COLUMNS = 3 # 列数 481 | 482 | ASTERISK_ONLY = False # 加载所有单词 483 | #ASTERISK_ONLY = True # 仅加载星号"*"标注的单词 484 | 485 | 486 | # 过滤星号标注的单词 487 | dic = [] 488 | for en, cn in dictionary: 489 | if en.startswith('*'): 490 | en = en.lstrip('*') 491 | dic.append( [en, cn] ) 492 | else: 493 | if not ASTERISK_ONLY: 494 | dic.append( [en, cn] ) 495 | dictionary = dic 496 | 497 | def generator(): 498 | global dictionary, RANDOM_MODE, COLUMNS 499 | line_cn, line_en = [], [] 500 | if len(dictionary) == 0: 501 | STOP() 502 | for i in range(COLUMNS): 503 | if len(dictionary) == 0: 504 | break 505 | if RANDOM_MODE: 506 | item = choice( dictionary ) 507 | dictionary.remove(item) 508 | else: 509 | item = dictionary.pop(0) 510 | en, cn = item 511 | line_cn.append( cn ) 512 | line_en.append( blank_word(en, skip_leading=1) ) # 提示首字母 513 | #line_en.append( blank_word(en, skip_leading=0) ) # 不提示首字母 514 | return line_cn + ['EOL'] + line_en + ['EOL'] 515 | 516 | '''], 517 | 518 | 519 | ['英语完形填空', '''\ 520 | INPUT = load_file('完形填空.txt') 521 | 522 | BLANK_NUM = 10 # 填空数量 523 | WORD_MIN_LEN = 3 # 填空单词最小字符数 524 | BLANK_LEN_MIN = 10 # 填空长度 525 | SEPERATE_WORDS_MIN_NUM = 2 # 两个填空之间最小分隔单词数量 526 | SHOW_LEADING_LETTER = False # 不显示单词首字母 527 | #SHOW_LEADING_LETTER = True # 显示单词首字母 528 | WORD_EXCEPTIONS = [] 529 | WORD_EXCEPTIONS_FILE = '完形填空排除的单词.txt' # 附加的排除的单词列表文件 530 | if os.path.isfile( WORD_EXCEPTIONS_FILE ): 531 | for l in load_file(WORD_EXCEPTIONS_FILE): 532 | WORD_EXCEPTIONS += get_line_words(l) 533 | 534 | # 预处理 535 | WORD_PAT = re.compile('[a-zA-Z0-9][a-zA-Z0-9-\\']*') 536 | class Word(): 537 | def __init__( self, s, is_word=True, valid=True, blank=False ): 538 | self.s = s 539 | self.is_word = is_word 540 | self.valid = valid 541 | self.blank = blank 542 | self.sol = False 543 | self.eol = False 544 | def __str__( self ): 545 | global BLANK_LEN_MIN, SHOW_LEADING_LETTER 546 | if not self.valid: 547 | return self.s 548 | else: 549 | if self.blank: 550 | underline = '_'*max(BLANK_LEN_MIN, len(self.s)+2) 551 | if SHOW_LEADING_LETTER: 552 | return self.s[0] + underline[1:] 553 | else: 554 | return underline 555 | else: 556 | return self.s # self.s.join('()') 557 | 558 | LIST = [] 559 | for l in INPUT: 560 | words = WORD_PAT.findall( l ) 561 | words_split = WORD_PAT.split( l ) 562 | LIST.append(Word(words_split[0], is_word=False, valid=False)) 563 | words_len = len(words) 564 | for i in range(words_len): 565 | w = words[i] 566 | if w in WORD_EXCEPTIONS: 567 | valid = False 568 | elif w.lower() in WORD_EXCEPTIONS: 569 | valid = False 570 | elif len(w) < WORD_MIN_LEN: 571 | valid = False 572 | else: 573 | valid = True 574 | _w = Word(w, valid=valid ) 575 | if i == 0: 576 | _w.sol = True 577 | elif i == words_len-1: 578 | _w.eol = True 579 | LIST.append( _w ) 580 | LIST.append( Word(words_split[i+1], is_word=False, valid=False) ) 581 | LIST.append( Word('\\n', valid=False) ) 582 | #print ''.join( [str(i) for i in LIST] ) 583 | 584 | # 选择填空 585 | SELECTED_WORDS = [] 586 | blank_counter = 0 587 | list_len = len(LIST) 588 | while blank_counter < BLANK_NUM: 589 | idx = randint(0, list_len-1) 590 | if not LIST[idx].valid: 591 | continue 592 | if LIST[idx].s[-1] in '-\\'': 593 | continue 594 | # 检查左侧单词 595 | if not LIST[idx].sol: 596 | valid, sep_chk_cnt, i = True, 0, idx 597 | while sep_chk_cnt < SEPERATE_WORDS_MIN_NUM: 598 | if i == 0: 599 | break 600 | i -= 1 601 | if not LIST[i].is_word: 602 | continue 603 | sep_chk_cnt += 1 604 | if LIST[i].blank: 605 | valid = False 606 | print ' left check fail', LIST[i].s, '<--', LIST[idx] 607 | break 608 | if LIST[i].sol: 609 | break 610 | if not valid: 611 | continue 612 | # 检查右侧单词 613 | if not LIST[idx].eol: 614 | valid, sep_chk_cnt, i = True, 0, idx 615 | while sep_chk_cnt < SEPERATE_WORDS_MIN_NUM: 616 | if i == list_len-1: 617 | break 618 | i += 1 619 | if not LIST[i].is_word: 620 | continue 621 | sep_chk_cnt += 1 622 | if LIST[i].blank: 623 | valid = False 624 | print ' right check fail', LIST[idx], '-->', LIST[i].s 625 | break 626 | if LIST[i].eol: 627 | break 628 | if not valid: 629 | continue 630 | # 过滤掉重复单词 631 | if LIST[idx].s.lower() in SELECTED_WORDS: 632 | continue 633 | print LIST[idx] 634 | LIST[idx].blank = True 635 | SELECTED_WORDS.append( LIST[idx].s.lower() ) 636 | blank_counter += 1 637 | 638 | print SELECTED_WORDS 639 | OUTPUT = ''.join([unicode(i) for i in LIST]).splitlines() 640 | def generator(): 641 | global OUTPUT 642 | if not OUTPUT: 643 | STOP() 644 | return [ OUTPUT.pop(0) ] 645 | 646 | '''], 647 | 648 | 649 | ['成语CROSSWORD', '''\ 650 | INPUT = [] 651 | for l in load_file('成语列表.txt'): 652 | for i in get_line_words(l): 653 | if not i in INPUT: 654 | INPUT.append(i) 655 | shuffle(INPUT) 656 | gen = Genxword(auto=True) 657 | gen.wlist(INPUT, len(INPUT)) 658 | 659 | ROW, COL = 10, 10 660 | calc = Crossword( ROW, COL, '', gen.wordlist ) 661 | calc.compute_crossword() 662 | 663 | RESULT = calc.best_grid 664 | BLANKED = [['' for c in range(COL)] for r in range(ROW)] 665 | for r in range(ROW): 666 | for c in range(COL): 667 | v = RESULT[r][c] 668 | # 检查相邻位置是否有内容,判断是否为交叉位置 669 | cnt_v, cnt_h = 0, 0 670 | if r > 0 and RESULT[r-1][c]: 671 | cnt_v += 1 672 | if r < ROW-1 and RESULT[r+1][c]: 673 | cnt_v += 1 674 | if c < COL-1 and RESULT[r][c+1]: 675 | cnt_h += 1 676 | if c > 0 and RESULT[r][c-1]: 677 | cnt_h += 1 678 | if v and cnt_v*cnt_h==0: 679 | v = "□" 680 | BLANKED[r][c] = v 681 | 682 | OUTPUT = BLANKED + [''] + RESULT 683 | def generator(): 684 | global OUTPUT 685 | if not OUTPUT: 686 | STOP() 687 | return OUTPUT.pop(0) 688 | '''], 689 | 690 | 691 | ['小数加减', '''\ 692 | def S(val): 693 | # 简化 694 | if int(val) == float(val): 695 | return int(val) 696 | else: 697 | return val 698 | 699 | def generator(): 700 | global S 701 | MIN, MAX = 0.0, 10.0 702 | DIGITS = 1 703 | mul = 10**DIGITS 704 | a = randint(MIN*mul, MAX*mul) / float(mul) 705 | b = randint(MIN*mul, MAX*mul) / float(mul) 706 | oper = choice( '+-' ) 707 | if oper == '+': 708 | c = a + b 709 | else: 710 | c = a - b 711 | ASSERT( 0 <= c <= MAX ) 712 | #return [ a, oper, b, '=', '□' ] 713 | return [ S(a), oper, S(b), '=', S(c) ] 714 | '''], 715 | 716 | 717 | ['分数运算', '''\ 718 | def gcd(a, b): 719 | while b != 0: 720 | a, b = b, a % b 721 | return a 722 | 723 | def simplify(a,b): 724 | global gcd 725 | div, _a = a/b, a%b 726 | d = gcd(_a,b) 727 | x, y = _a / d, b / d 728 | print '%d/%d'%(a,b), 'gcd=%d'%d, '->', '%d %d/%d'%(div, x, y) 729 | return div, x, y 730 | 731 | # (A+(B/C)) (+/-/*//) (D+(E/F)) = X+Y/Z 732 | def generator(): 733 | global simplify 734 | RESULT_DIV_MAX = 50 # 计算结果分母上限 735 | MIN, MAX = 1, 20 736 | _b = randint(MIN, MAX) 737 | _c = randint(2, MAX) 738 | _e = randint(MIN, MAX) 739 | _f = randint(2, MAX) 740 | a, b, c = simplify( _b, _c ) 741 | ASSERT( b != 0 ) 742 | d, e, f = simplify( _e, _f ) 743 | ASSERT( e != 0 ) 744 | 745 | #ASSERT( c == f ) # 同分母 746 | oper = choice( '+-×÷' ) 747 | if oper == '+': 748 | x, y, z = simplify( _b*_f+_c*_e, _c*_f ) 749 | elif oper == '-': 750 | ASSERT( float(a)+float(b)/float(c) >= float(d)+float(e)/float(f) ) # 不允许负数 751 | x, y, z = simplify( _b*_f-_c*_e, _c*_f ) 752 | elif oper == '×': 753 | x, y, z = simplify( _b*_e, _e*_f ) 754 | elif oper == '÷': 755 | ASSERT( _e != 0 ) 756 | x, y, z = simplify( _b*_f, _c*_e ) 757 | ASSERT( z <= RESULT_DIV_MAX ) 758 | if a == 0: 759 | a = '' 760 | if d == 0: 761 | d = '' 762 | if x == 0: 763 | x = '' 764 | return [ '', b, '', '', e, '', '', y, 'EOL', 765 | a, '──', oper, d, '──', '=', x, '──', 'EOL', 766 | '', c, '', '', f, '', '', z, 'EOL' ] 767 | '''], 768 | 769 | ] 770 | 771 | CONFIGS_DICT = {} 772 | for k,v in CONFIGS_LIST: 773 | CONFIGS_DICT[k] = v 774 | 775 | ############################################################################### 776 | 777 | class AssertError(Exception): 778 | pass 779 | 780 | class StopError(Exception): 781 | pass 782 | 783 | 784 | def ASSERT(condition): 785 | if not condition: 786 | raise AssertError() 787 | 788 | def STOP(message=''): 789 | raise StopError(message) 790 | 791 | 792 | def load_cn_words(fname): 793 | ret=[] 794 | if not os.path.isfile(fname): 795 | STOP('文件 %s 不存在'% fname) 796 | for l in open(fname,'r').read().decode(encoding='utf8').splitlines(): 797 | if l.startswith('\ufeff'): 798 | l = l.lstrip('\ufeff') 799 | l = l.split('#')[0].strip() 800 | if not l: 801 | continue 802 | for w in l.split(' '): 803 | if not w or w.startswith('~'): 804 | continue 805 | if w in ret: 806 | continue 807 | ret.append(w) 808 | return ret 809 | 810 | def load_en_words(fname): 811 | ret=[] 812 | if not os.path.isfile(fname): 813 | STOP('文件 %s 不存在'% fname) 814 | for l in open(fname,'r').read().decode(encoding='utf8').splitlines(): 815 | if l.startswith('\ufeff'): 816 | l = l.lstrip('\ufeff') 817 | l = l.split('#')[0].strip() 818 | if not l: 819 | continue 820 | splits = l.split(' ') 821 | if len(splits) < 2: 822 | continue 823 | cn = splits.pop().strip() 824 | en = ''.join(splits).strip() 825 | if (not en) or (not cn): 826 | continue 827 | cn_splits = cn.split('.') 828 | if len(cn_splits) > 1: 829 | if cn_splits[0] in ['adv','adj','conj','aux','interj','v','n','num','prep','pron']: 830 | cn = ''.join(cn_splits[1:]) 831 | ret.append( [en, cn] ) 832 | return ret 833 | 834 | def load_file(fname): 835 | ret=[] 836 | if not os.path.isfile(fname): 837 | STOP('文件 %s 不存在'% fname) 838 | raw = open(fname,'r').read().decode(encoding='utf8') 839 | if raw.startswith('\ufeff'): 840 | raw = raw.lstrip('\ufeff') 841 | for l in raw.splitlines(): 842 | ret.append( l.rstrip() ) 843 | return ret 844 | 845 | def blank_word(word, space_seperator=True, skip_leading=1): 846 | word_list = list(word) 847 | for i in range(skip_leading, len(word_list)): 848 | if word_list[i].isalpha(): 849 | word_list[i] = ' _' if space_seperator else '_' 850 | return ''.join(word_list) 851 | 852 | def get_line_words(line): 853 | ret = [] 854 | for i in line.split(): 855 | i = i.strip() 856 | if i: 857 | ret.append(i) 858 | return ret 859 | 860 | def getRMBName(m): 861 | if m < 1.0: 862 | return '%d角'% (10*m) 863 | elif m-floor(m): 864 | return '%d元%d角'% (floor(m), 10*m-10*floor(m)) 865 | else: 866 | return '%d元'% (floor(m)) 867 | 868 | def check_traceback_for_errline(e): 869 | #traceback.print_exc(sys.stdout) 870 | err = [] 871 | for line in traceback.format_exc().splitlines(): 872 | line = line.rstrip() 873 | if line: 874 | err.append(line) 875 | err_info = err.pop() 876 | err_file, err_line, err_module = None, None, None 877 | while err: 878 | l = err.pop() 879 | try: 880 | if not l.startswith(' File '): 881 | continue 882 | except UnicodeDecodeError: 883 | continue 884 | try: 885 | _file, _line, _module = l.split(', ') 886 | except ValueError: 887 | _file, _line = l.split(', ') 888 | _module = '' 889 | err_module = _module.lstrip('in ') 890 | if err_module == '': 891 | err_file = _file.lstrip(' File ') 892 | err_line = int(_line.lstrip('line ')) 893 | break 894 | return err_line 895 | 896 | 897 | class MainDialog(MyDialog): 898 | 899 | def __init__(self, *args, **kwds): 900 | MyDialog.__init__( self, *args, **kwds ) 901 | self.Bind(wx.EVT_CLOSE, self.OnClose, self) 902 | self.init_text_ctrl_rules() 903 | self.button_generate.Enable(False) 904 | self.button_copy_result.Enable(False) 905 | self.combo_box_type.SetValue('请选择出题类型...') 906 | self.combo_box_type.AppendItems([c[0] for c in CONFIGS_LIST]) 907 | self.text_ctrl_number.SetValue('100') 908 | self.clrAllResult() 909 | 910 | def OnClose(self, event): 911 | self.Destroy() 912 | event.Skip() 913 | 914 | def OnSelectType(self, event): 915 | self.info('') 916 | tp = self.combo_box_type.GetValue() 917 | if CONFIGS_DICT.has_key(tp): 918 | self.text_ctrl_rules.SetValue( CONFIGS_DICT[tp] ) 919 | self.text_ctrl_rules.EmptyUndoBuffer() 920 | self.text_ctrl_rules.MarkerDeleteAll(0) 921 | self.button_generate.Enable(True) 922 | else: 923 | self.button_generate.Enable(False) 924 | event.Skip() 925 | 926 | def clrAllResult( self ): 927 | self.grid_result.ClearGrid() 928 | lines = self.grid_result.GetNumberRows() 929 | if lines: 930 | self.grid_result.DeleteRows(numRows=lines) 931 | 932 | def addResult( self, result ): 933 | if self.grid_result.AppendRows(): 934 | line = self.grid_result.GetNumberRows() 935 | index = 0 936 | for item in result: 937 | if isinstance(item, unicode) and item == 'EOL': 938 | if self.grid_result.AppendRows(): 939 | line += 1 940 | index = 0 941 | else: 942 | return 943 | else: 944 | self.grid_result.SetCellValue( line-1, index, unicode(item) ) 945 | index += 1 946 | else: 947 | return 948 | 949 | def info( self, info, info_type=wx.ICON_WARNING ): 950 | if info: 951 | self.window_info.ShowMessage(info, info_type) 952 | else: 953 | self.window_info.Dismiss() 954 | 955 | def OnGenerate(self, event): 956 | self.info('') 957 | self.text_ctrl_rules.MarkerDeleteAll(0) 958 | rules = self.text_ctrl_rules.GetValue() 959 | try: 960 | num = int(self.text_ctrl_number.GetValue()) 961 | if num <= 0 or num > 10000: 962 | raise Exception 963 | except: 964 | self.info('出题数量错误', wx.ICON_ERROR) 965 | return 966 | counter = 0 967 | try: 968 | code = compile(rules, '', 'exec') 969 | exec( code ) 970 | generator 971 | except StopError as e: 972 | err_line = check_traceback_for_errline(e) 973 | if err_line is not None: 974 | self.text_ctrl_rules.MarkerAdd( err_line-1, 0 ) 975 | self.text_ctrl_rules.GotoLine( err_line-1 ) 976 | self.info(unicode(e), wx.ICON_ERROR) 977 | return 978 | except Exception as e: 979 | err_line = check_traceback_for_errline(e) 980 | if err_line is not None: 981 | self.text_ctrl_rules.MarkerAdd( err_line-1, 0 ) 982 | self.text_ctrl_rules.GotoLine( err_line-1 ) 983 | self.info(unicode(e), wx.ICON_ERROR) 984 | return 985 | self.clrAllResult() 986 | t0 = time.time() 987 | while counter < num: 988 | try: 989 | result = generator() 990 | counter += 1 991 | self.addResult( result ) 992 | except AssertError: 993 | pass 994 | except StopError as e: 995 | err_line = check_traceback_for_errline(e) 996 | if err_line is not None: 997 | self.text_ctrl_rules.MarkerAdd( err_line-1, 0 ) 998 | self.text_ctrl_rules.GotoLine( err_line-1 ) 999 | self.info(unicode(e), wx.ICON_ERROR) 1000 | break 1001 | except Exception as e: 1002 | err_line = check_traceback_for_errline(e) 1003 | if err_line is not None: 1004 | self.text_ctrl_rules.MarkerAdd( err_line-1, 0 ) 1005 | self.text_ctrl_rules.GotoLine( err_line-1 ) 1006 | self.info(unicode(e)) 1007 | return 1008 | t1 = time.time() 1009 | print( 'time elapsed: %.1f seconds'% (t1-t0) ) 1010 | self.button_copy_result.Enable(True) 1011 | event.Skip() 1012 | 1013 | def OnCopyResult(self, event): 1014 | self.info('') 1015 | lines = self.grid_result.GetNumberRows() 1016 | if lines: 1017 | ret = [] 1018 | for l in range(lines): 1019 | items = [self.grid_result.GetCellValue( l, c ) \ 1020 | for c in range(26)] 1021 | cp = '\t'.join(items).rstrip('\t') 1022 | print cp 1023 | ret.append( cp ) 1024 | copy = '\r\n'.join(ret) 1025 | import pyperclip 1026 | pyperclip.copy( copy ) 1027 | event.Skip() 1028 | 1029 | def OnAbout(self, event): 1030 | dlg = wx.MessageDialog(self, ABOUT_INFO, '关于', wx.OK) 1031 | dlg.ShowModal() 1032 | dlg.Destroy() 1033 | 1034 | def init_text_ctrl_rules(self): 1035 | ctrl = self.text_ctrl_rules 1036 | faces = { 'times': 'Courier New', 1037 | 'mono' : 'Courier New', 1038 | 'helv' : 'Courier New', 1039 | 'other': 'Courier New', 1040 | 'size' : 12, 1041 | 'size2': 10, 1042 | } 1043 | ctrl.SetLexer(wx.stc.STC_LEX_PYTHON) 1044 | ctrl.SetKeyWords(0, " ".join(keyword.kwlist)) 1045 | ctrl.SetProperty("tab.timmy.whinge.level", "1") 1046 | ctrl.SetMargins(0,0) 1047 | ctrl.SetIndent(4) 1048 | ctrl.SetIndentationGuides(True) 1049 | ctrl.SetBackSpaceUnIndents(True) 1050 | ctrl.SetTabIndents(True) 1051 | ctrl.SetTabWidth(4) 1052 | ctrl.SetUseTabs(False) 1053 | ctrl.SetViewWhiteSpace(False) 1054 | ctrl.Bind(wx.stc.EVT_STC_UPDATEUI, self.OnUpdateUI) 1055 | ctrl.StyleSetSpec(wx.stc.STC_STYLE_DEFAULT, "face:%(helv)s,size:%(size)d" % faces) 1056 | ctrl.StyleClearAll() 1057 | ctrl.StyleSetSpec(wx.stc.STC_STYLE_DEFAULT, "face:%(helv)s,size:%(size)d" % faces) 1058 | ctrl.StyleSetSpec(wx.stc.STC_STYLE_LINENUMBER, "back:#C0C0C0,face:%(helv)s,size:%(size2)d" % faces) 1059 | ctrl.StyleSetSpec(wx.stc.STC_STYLE_CONTROLCHAR, "face:%(other)s" % faces) 1060 | ctrl.StyleSetSpec(wx.stc.STC_STYLE_BRACELIGHT, "fore:#FFFFFF,back:#0000FF,bold") 1061 | ctrl.StyleSetSpec(wx.stc.STC_STYLE_BRACEBAD, "fore:#000000,back:#FF0000,bold") 1062 | ctrl.StyleSetSpec(wx.stc.STC_P_DEFAULT, "fore:#000000,face:%(helv)s,size:%(size)d" % faces) 1063 | ctrl.StyleSetSpec(wx.stc.STC_P_COMMENTLINE, "fore:#007F00,face:%(other)s,size:%(size)d" % faces) 1064 | ctrl.StyleSetSpec(wx.stc.STC_P_NUMBER, "fore:#007F7F,size:%(size)d" % faces) 1065 | ctrl.StyleSetSpec(wx.stc.STC_P_STRING, "fore:#7F007F,face:%(helv)s,size:%(size)d" % faces) 1066 | ctrl.StyleSetSpec(wx.stc.STC_P_CHARACTER, "fore:#7F007F,face:%(helv)s,size:%(size)d" % faces) 1067 | ctrl.StyleSetSpec(wx.stc.STC_P_WORD, "fore:#00007F,bold,size:%(size)d" % faces) 1068 | ctrl.StyleSetSpec(wx.stc.STC_P_TRIPLE, "fore:#7F0000,size:%(size)d" % faces) 1069 | ctrl.StyleSetSpec(wx.stc.STC_P_TRIPLEDOUBLE, "fore:#7F0000,size:%(size)d" % faces) 1070 | ctrl.StyleSetSpec(wx.stc.STC_P_CLASSNAME, "fore:#0000FF,bold,underline,size:%(size)d" % faces) 1071 | ctrl.StyleSetSpec(wx.stc.STC_P_DEFNAME, "fore:#007F7F,bold,size:%(size)d" % faces) 1072 | ctrl.StyleSetSpec(wx.stc.STC_P_OPERATOR, "bold,size:%(size)d" % faces) 1073 | ctrl.StyleSetSpec(wx.stc.STC_P_IDENTIFIER, "fore:#000000,face:%(helv)s,size:%(size)d" % faces) 1074 | ctrl.StyleSetSpec(wx.stc.STC_P_COMMENTBLOCK, "fore:#7F7F7F,size:%(size)d" % faces) 1075 | ctrl.StyleSetSpec(wx.stc.STC_P_STRINGEOL, "fore:#000000,face:%(mono)s,back:#E0C0E0,eol,size:%(size)d" % faces) 1076 | ctrl.SetCaretForeground("BLACK") 1077 | ctrl.SetMarginType(1, wx.stc.STC_MARGIN_NUMBER) 1078 | ctrl.SetMarginWidth(1, 50) 1079 | ctrl.MarkerDefine(0, wx.stc.STC_MARK_ARROW, "blue", "blue") 1080 | self.SetAcceleratorTable(wx.AcceleratorTable([ 1081 | (wx.ACCEL_NORMAL, wx.WXK_F5, self.button_generate.GetId()), # run 1082 | ])) 1083 | 1084 | 1085 | def OnUpdateUI(self, evt): 1086 | ctrl = self.text_ctrl_rules 1087 | # check for matching braces 1088 | braceAtCaret = -1 1089 | braceOpposite = -1 1090 | charBefore = None 1091 | caretPos = ctrl.GetCurrentPos() 1092 | 1093 | if caretPos > 0: 1094 | charBefore = ctrl.GetCharAt(caretPos - 1) 1095 | styleBefore = ctrl.GetStyleAt(caretPos - 1) 1096 | 1097 | # check before 1098 | if charBefore and (32 < charBefore < 128): 1099 | if chr(charBefore) in "[]{}()" and styleBefore == wx.stc.STC_P_OPERATOR: 1100 | braceAtCaret = caretPos - 1 1101 | 1102 | # check after 1103 | if braceAtCaret < 0: 1104 | charAfter = ctrl.GetCharAt(caretPos) 1105 | styleAfter = ctrl.GetStyleAt(caretPos) 1106 | if charAfter and (32 < charAfter < 128): 1107 | if chr(charAfter) in "[]{}()" and styleAfter == wx.stc.STC_P_OPERATOR: 1108 | braceAtCaret = caretPos 1109 | 1110 | if braceAtCaret >= 0: 1111 | braceOpposite = ctrl.BraceMatch(braceAtCaret) 1112 | if braceAtCaret != -1 and braceOpposite == -1: 1113 | ctrl.BraceBadLight(braceAtCaret) 1114 | else: 1115 | ctrl.BraceHighlight(braceAtCaret, braceOpposite) 1116 | 1117 | 1118 | 1119 | if __name__ == "__main__": 1120 | gettext.install("app") 1121 | app = wx.App(0) 1122 | app.SetAppName( 'ExerciseGeneratorApp' ) 1123 | dialog_1 = MainDialog(None, wx.ID_ANY, "") 1124 | app.SetTopWindow(dialog_1) 1125 | dialog_1.Show() 1126 | app.MainLoop() 1127 | -------------------------------------------------------------------------------- /ExerciseGeneratorDlg.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: UTF-8 -*- 3 | # 4 | # generated by wxGlade f172c83ff51d+ on Sun Oct 1 20:14:19 2017 5 | # 6 | 7 | import wx 8 | import wx.grid 9 | 10 | # begin wxGlade: dependencies 11 | import gettext 12 | # end wxGlade 13 | 14 | # begin wxGlade: extracode 15 | from wx import InfoBar 16 | import wx.stc 17 | from wx.stc import StyledTextCtrl 18 | # end wxGlade 19 | 20 | 21 | class MyDialog(wx.Dialog): 22 | def __init__(self, *args, **kwds): 23 | # begin wxGlade: MyDialog.__init__ 24 | kwds["style"] = wx.DEFAULT_DIALOG_STYLE | wx.MAXIMIZE_BOX | wx.MINIMIZE_BOX | wx.RESIZE_BORDER 25 | wx.Dialog.__init__(self, *args, **kwds) 26 | self.combo_box_type = wx.ComboBox(self, wx.ID_ANY, choices=[], style=wx.CB_DROPDOWN) 27 | self.text_ctrl_number = wx.TextCtrl(self, wx.ID_ANY, _("100")) 28 | self.text_ctrl_rules = StyledTextCtrl(self, wx.ID_ANY) 29 | self.window_info = InfoBar(self, wx.ID_ANY) 30 | self.button_generate = wx.Button(self, wx.ID_ANY, _(u"\u51fa\u9898")) 31 | self.button_copy_result = wx.Button(self, wx.ID_ANY, _(u"\u590d\u5236\u7ed3\u679c")) 32 | self.button_about = wx.Button(self, wx.ID_ANY, _(u"\u5173\u4e8e")) 33 | self.grid_result = wx.grid.Grid(self, wx.ID_ANY, size=(1, 1)) 34 | 35 | self.__set_properties() 36 | self.__do_layout() 37 | 38 | self.Bind(wx.EVT_COMBOBOX, self.OnSelectType, self.combo_box_type) 39 | self.Bind(wx.EVT_BUTTON, self.OnGenerate, self.button_generate) 40 | self.Bind(wx.EVT_BUTTON, self.OnCopyResult, self.button_copy_result) 41 | self.Bind(wx.EVT_BUTTON, self.OnAbout, self.button_about) 42 | # end wxGlade 43 | 44 | def __set_properties(self): 45 | # begin wxGlade: MyDialog.__set_properties 46 | self.SetTitle(_(u"\u81ea\u52a8\u51fa\u9898\u673a")) 47 | self.SetSize((1000, 700)) 48 | self.button_generate.SetMinSize((100, -1)) 49 | self.grid_result.CreateGrid(100, 26) 50 | self.grid_result.EnableEditing(0) 51 | # end wxGlade 52 | 53 | def __do_layout(self): 54 | # begin wxGlade: MyDialog.__do_layout 55 | sizer_1 = wx.BoxSizer(wx.HORIZONTAL) 56 | sizer_2 = wx.BoxSizer(wx.VERTICAL) 57 | sizer_3 = wx.BoxSizer(wx.HORIZONTAL) 58 | sizer_4 = wx.BoxSizer(wx.VERTICAL) 59 | sizer_5 = wx.BoxSizer(wx.VERTICAL) 60 | sizer_9 = wx.BoxSizer(wx.HORIZONTAL) 61 | sizer_7 = wx.BoxSizer(wx.VERTICAL) 62 | sizer_6 = wx.BoxSizer(wx.HORIZONTAL) 63 | sizer_1.Add((20, 20), 0, 0, 0) 64 | sizer_2.Add((20, 20), 0, 0, 0) 65 | label_1 = wx.StaticText(self, wx.ID_ANY, _(u"\u9009\u62e9\u9898\u578b\uff1a")) 66 | sizer_6.Add(label_1, 0, wx.ALIGN_CENTER_VERTICAL, 0) 67 | sizer_6.Add(self.combo_box_type, 1, wx.ALIGN_CENTER_VERTICAL | wx.EXPAND, 0) 68 | sizer_6.Add((20, 20), 0, 0, 0) 69 | label_5 = wx.StaticText(self, wx.ID_ANY, _(u"\u51fa\u9898\u6570\u91cf\uff1a")) 70 | sizer_6.Add(label_5, 0, wx.ALIGN_CENTER_VERTICAL, 0) 71 | sizer_6.Add(self.text_ctrl_number, 0, 0, 0) 72 | sizer_5.Add(sizer_6, 0, wx.EXPAND, 0) 73 | sizer_5.Add((20, 20), 0, 0, 0) 74 | label_4 = wx.StaticText(self, wx.ID_ANY, _(u"\u89c4\u5219\u5b9a\u4e49\uff1a")) 75 | sizer_9.Add(label_4, 0, wx.ALIGN_CENTER_VERTICAL, 0) 76 | sizer_7.Add(self.text_ctrl_rules, 1, wx.EXPAND, 0) 77 | sizer_7.Add(self.window_info, 0, wx.EXPAND, 0) 78 | sizer_9.Add(sizer_7, 1, wx.EXPAND, 0) 79 | sizer_5.Add(sizer_9, 1, wx.EXPAND, 0) 80 | sizer_3.Add(sizer_5, 1, wx.EXPAND, 0) 81 | sizer_3.Add((20, 20), 0, 0, 0) 82 | sizer_4.Add(self.button_generate, 2, wx.EXPAND, 0) 83 | sizer_4.Add((20, 20), 0, 0, 0) 84 | sizer_4.Add(self.button_copy_result, 1, wx.EXPAND, 0) 85 | sizer_4.Add((20, 20), 0, 0, 0) 86 | sizer_4.Add(self.button_about, 1, wx.EXPAND, 0) 87 | sizer_3.Add(sizer_4, 0, wx.EXPAND, 0) 88 | sizer_2.Add(sizer_3, 2, wx.EXPAND, 0) 89 | sizer_2.Add((20, 20), 0, 0, 0) 90 | sizer_2.Add(self.grid_result, 1, wx.EXPAND, 0) 91 | sizer_2.Add((20, 20), 0, 0, 0) 92 | sizer_1.Add(sizer_2, 1, wx.EXPAND, 0) 93 | sizer_1.Add((20, 20), 0, 0, 0) 94 | self.SetSizer(sizer_1) 95 | self.Layout() 96 | self.Centre() 97 | # end wxGlade 98 | 99 | def OnSelectType(self, event): # wxGlade: MyDialog. 100 | print("Event handler 'OnSelectType' not implemented!") 101 | event.Skip() 102 | 103 | def OnGenerate(self, event): # wxGlade: MyDialog. 104 | print("Event handler 'OnGenerate' not implemented!") 105 | event.Skip() 106 | 107 | def OnCopyResult(self, event): # wxGlade: MyDialog. 108 | print("Event handler 'OnCopyResult' not implemented!") 109 | event.Skip() 110 | 111 | def OnAbout(self, event): # wxGlade: MyDialog. 112 | print("Event handler 'OnAbout' not implemented!") 113 | event.Skip() 114 | 115 | # end of class MyDialog 116 | -------------------------------------------------------------------------------- /ExerciseGeneratorDlg.wxg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 1000, 700 7 | 自动出题机 8 | 9 | 1 10 | 11 | wxHORIZONTAL 12 | 13 | 14 | 0 15 | 16 | 20 17 | 20 18 | 19 | 20 | 21 | 22 | 0 23 | wxEXPAND 24 | 25 | wxVERTICAL 26 | 27 | 28 | 0 29 | 30 | 20 31 | 20 32 | 33 | 34 | 35 | 36 | 0 37 | wxEXPAND 38 | 39 | wxHORIZONTAL 40 | 41 | 42 | 0 43 | wxEXPAND 44 | 45 | wxVERTICAL 46 | 47 | 48 | 0 49 | wxEXPAND 50 | 51 | wxHORIZONTAL 52 | 53 | 54 | 0 55 | wxALIGN_CENTER_VERTICAL 56 | 57 | 58 | 59 | 60 | 61 | 62 | 0 63 | wxEXPAND|wxALIGN_CENTER_VERTICAL 64 | 65 | 66 | OnSelectType 67 | 68 | 69 | -1 70 | 71 | 72 | 73 | 74 | 75 | 76 | 0 77 | 78 | 20 79 | 20 80 | 81 | 82 | 83 | 84 | 0 85 | wxALIGN_CENTER_VERTICAL 86 | 87 | 88 | 89 | 90 | 91 | 92 | 0 93 | 94 | 100 95 | 96 | 97 | 98 | 99 | 100 | 101 | 0 102 | 103 | 20 104 | 20 105 | 106 | 107 | 108 | 109 | 0 110 | wxEXPAND 111 | 112 | wxHORIZONTAL 113 | 114 | 115 | 0 116 | wxALIGN_CENTER_VERTICAL 117 | 118 | 119 | 120 | 121 | 122 | 123 | 0 124 | wxEXPAND 125 | 126 | wxVERTICAL 127 | 128 | 129 | 0 130 | wxEXPAND 131 | 132 | import wx.stc\nfrom wx.stc import StyledTextCtrl 133 | 134 | $parent 135 | $id 136 | 137 | 138 | 139 | 140 | 141 | 0 142 | wxEXPAND 143 | 144 | from wx import InfoBar 145 | 146 | $parent 147 | $id 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 0 160 | 161 | 20 162 | 20 163 | 164 | 165 | 166 | 167 | 0 168 | wxEXPAND 169 | 170 | wxVERTICAL 171 | 172 | 173 | 0 174 | wxEXPAND 175 | 176 | 177 | OnGenerate 178 | 179 | 100, -1 180 | 181 | 182 | 183 | 184 | 185 | 0 186 | 187 | 20 188 | 20 189 | 190 | 191 | 192 | 193 | 0 194 | wxEXPAND 195 | 196 | 197 | OnCopyResult 198 | 199 | 200 | 201 | 202 | 203 | 204 | 0 205 | 206 | 20 207 | 20 208 | 209 | 210 | 211 | 212 | 0 213 | wxEXPAND 214 | 215 | 216 | OnAbout 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 0 228 | 229 | 20 230 | 20 231 | 232 | 233 | 234 | 235 | 0 236 | wxEXPAND 237 | 238 | 1 239 | 240 | A 241 | B 242 | C 243 | D 244 | E 245 | F 246 | G 247 | H 248 | I 249 | J 250 | K 251 | L 252 | M 253 | N 254 | O 255 | P 256 | Q 257 | R 258 | S 259 | T 260 | U 261 | V 262 | W 263 | X 264 | Y 265 | Z 266 | 267 | 100 268 | 0 269 | 1 270 | 1 271 | 1 272 | 1 273 | wxGrid.wxGridSelectCells 274 | 275 | 276 | 277 | 278 | 0 279 | 280 | 20 281 | 20 282 | 283 | 284 | 285 | 286 | 287 | 288 | 0 289 | 290 | 20 291 | 20 292 | 293 | 294 | 295 | 296 | 297 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | introduction in Chinese, no English version 2 | 3 | Python练习题生成器 4 | ================== 5 | 6 | 个人业余制作,给自家娃出题训练用。 7 | 8 | 用Python定义规则,将生成结果复制粘帖到Excel/WPS模板中再排版,此处只提供工具,暂不提供模板文件。 9 | 10 | 规则说明: 11 | 12 | * 参考“关于”按钮。 13 | 14 | 15 | 截图 16 | 17 | ![](snapshots/demo.png) 18 | 19 | 20 | URL: 21 | 22 | Peng Shullin 2019 23 | 24 | 25 | -------------------------------------------------------------------------------- /app_dist/exercise_generator_setup_v1.3.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengshulin/exercise_generator/27fb3863920acab26484ab533aefa5d850201ee8/app_dist/exercise_generator_setup_v1.3.exe -------------------------------------------------------------------------------- /app_dist/exercise_generator_setup_v1.4.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengshulin/exercise_generator/27fb3863920acab26484ab533aefa5d850201ee8/app_dist/exercise_generator_setup_v1.4.exe -------------------------------------------------------------------------------- /app_dist/exercise_generator_setup_v1.5.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengshulin/exercise_generator/27fb3863920acab26484ab533aefa5d850201ee8/app_dist/exercise_generator_setup_v1.5.exe -------------------------------------------------------------------------------- /build_exercise_generator_app.bat: -------------------------------------------------------------------------------- 1 | i: 2 | cd \github.exercise_generator\ 3 | 4 | python setup_ExerciseGeneratorApp.py py2exe 5 | "c:\Program Files\Inno Setup 5\Compil32.exe" /cc ExerciseGeneratorApp.iss 6 | 7 | pause 8 | -------------------------------------------------------------------------------- /clock/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengshulin/exercise_generator/27fb3863920acab26484ab533aefa5d850201ee8/clock/1.png -------------------------------------------------------------------------------- /clock/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengshulin/exercise_generator/27fb3863920acab26484ab533aefa5d850201ee8/clock/2.png -------------------------------------------------------------------------------- /clock/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengshulin/exercise_generator/27fb3863920acab26484ab533aefa5d850201ee8/clock/3.png -------------------------------------------------------------------------------- /clock/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengshulin/exercise_generator/27fb3863920acab26484ab533aefa5d850201ee8/clock/4.png -------------------------------------------------------------------------------- /clock/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengshulin/exercise_generator/27fb3863920acab26484ab533aefa5d850201ee8/clock/5.png -------------------------------------------------------------------------------- /clock/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengshulin/exercise_generator/27fb3863920acab26484ab533aefa5d850201ee8/clock/6.png -------------------------------------------------------------------------------- /clock/run.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: UTF-8 -*- 3 | # 运行前手工修改wx.lib.analogclock.helpers.HandSet._draw的生成时间规则 4 | import wx 5 | import wx.lib.analogclock as ac 6 | from random import randint, choice 7 | 8 | class MyDialog(wx.Dialog): 9 | def __init__(self, *args, **kwds): 10 | kwds["style"] = wx.DEFAULT_DIALOG_STYLE | wx.MAXIMIZE_BOX | wx.MINIMIZE_BOX | wx.RESIZE_BORDER 11 | wx.Dialog.__init__(self, *args, **kwds) 12 | self.Bind(wx.EVT_CLOSE, self.OnClose, self) 13 | 14 | def makeClock(parent, has_hand=True): 15 | style = ac.SHOW_HOURS_TICKS 16 | if has_hand: 17 | style |= ac.SHOW_HOURS_HAND | ac.SHOW_MINUTES_HAND 18 | ctrl = ac.AnalogClock(parent, size=(300,300), hoursStyle=ac.TICKS_DECIMAL, clockStyle=style ) 19 | ctrl.SetBackgroundColour(wx.Colour(255,255,255)) 20 | ctrl.SetFaceFillColour(wx.Colour(255,255,255)) 21 | return ctrl 22 | 23 | def makeText(parent, text='____:____'): 24 | ctrl = wx.TextCtrl(parent, wx.ID_ANY, text, style=wx.TE_CENTRE|wx.NO_BORDER ) 25 | ctrl.SetFont(wx.Font(12, wx.DEFAULT, wx.NORMAL, wx.NORMAL, 1, "")) 26 | ctrl.SetBackgroundColour(wx.Colour(255,255,255)) 27 | return ctrl 28 | 29 | ROW, COL = 4, 10 30 | sizer_clock=wx.GridSizer(ROW,COL,10,10) 31 | for i in range(ROW): 32 | for j in range(COL): 33 | sizer = wx.BoxSizer(wx.VERTICAL) 34 | sizer.Add( makeClock(self, False), 1, wx.EXPAND ) 35 | sizer.Add( (20, 10), 0, 0 ) 36 | h, m = randint(1,12), choice([0,30]) 37 | s = ' %d:%02d '%(h,m) 38 | sizer.Add( makeText(self, s), 0, wx.EXPAND ) 39 | sizer_clock.Add(sizer, 1, wx.EXPAND) 40 | 41 | sizer_main=wx.BoxSizer(wx.VERTICAL) 42 | sizer_main.Add( (20, 20), 0, 0 ) 43 | sizer_main.Add( sizer_clock, 1, wx.EXPAND ) 44 | sizer_main.Add( (20, 20), 0, 0 ) 45 | self.SetSizer(sizer_main) 46 | self.SetBackgroundColour(wx.Colour(255,255,255)) 47 | self.SetSize( (297*3, 210*3)) 48 | self.Center() 49 | self.ShowFullScreen(True) 50 | 51 | 52 | def OnClose(self, event): 53 | self.Destroy() 54 | event.Skip() 55 | 56 | 57 | if __name__ == "__main__": 58 | app = wx.App(0) 59 | dialog_1 = MyDialog(None, wx.ID_ANY, "") 60 | app.SetTopWindow(dialog_1) 61 | dialog_1.Show() 62 | app.MainLoop() 63 | 64 | 65 | -------------------------------------------------------------------------------- /genxword/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | -------------------------------------------------------------------------------- /genxword/calculate.py: -------------------------------------------------------------------------------- 1 | """Calculate the crossword and export image and text files.""" 2 | 3 | # Authors: David Whitlock , Bryan Helmig 4 | # Crossword generator that outputs the grid and clues as a pdf file and/or 5 | # the grid in png/svg format with a text file containing the words and clues. 6 | # Copyright (C) 2010-2011 Bryan Helmig 7 | # Copyright (C) 2011-2016 David Whitlock 8 | # 9 | # Genxword is free software: you can redistribute it and/or modify 10 | # it under the terms of the GNU General Public License as published by 11 | # the Free Software Foundation, either version 3 of the License, or 12 | # (at your option) any later version. 13 | # 14 | # Genxword is distributed in the hope that it will be useful, 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | # GNU General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU General Public License 20 | # along with genxword. If not, see . 21 | 22 | #from gi.repository import Pango, PangoCairo 23 | import random, time #, cairo 24 | from operator import itemgetter 25 | from collections import defaultdict 26 | 27 | import sys 28 | 29 | PY2 = sys.version_info[0] == 2 30 | if PY2: 31 | import codecs 32 | from functools import partial 33 | open = partial(codecs.open, encoding='utf-8') 34 | 35 | class Crossword(object): 36 | def __init__(self, rows, cols, empty=' ', available_words=[]): 37 | self.rows = rows 38 | self.cols = cols 39 | self.empty = empty 40 | self.available_words = available_words 41 | self.let_coords = defaultdict(list) 42 | 43 | def prep_grid_words(self): 44 | self.current_wordlist = [] 45 | self.let_coords.clear() 46 | self.grid = [[self.empty]*self.cols for i in range(self.rows)] 47 | self.available_words = [word[:2] for word in self.available_words] 48 | self.first_word(self.available_words[0]) 49 | 50 | def compute_crossword(self, time_permitted=1.00): 51 | self.best_wordlist = [] 52 | wordlist_length = len(self.available_words) 53 | time_permitted = float(time_permitted) 54 | start_full = float(time.time()) 55 | while (float(time.time()) - start_full) < time_permitted: 56 | self.prep_grid_words() 57 | [self.add_words(word) for i in range(2) for word in self.available_words 58 | if word not in self.current_wordlist] 59 | if len(self.current_wordlist) > len(self.best_wordlist): 60 | self.best_wordlist = list(self.current_wordlist) 61 | self.best_grid = list(self.grid) 62 | if len(self.best_wordlist) == wordlist_length: 63 | break 64 | #answer = '\n'.join([''.join(['{} '.format(c) for c in self.best_grid[r]]) for r in range(self.rows)]) 65 | answer = '\n'.join([''.join([u'{} '.format(c) for c in self.best_grid[r]]) 66 | for r in range(self.rows)]) 67 | return answer + '\n\n' + str(len(self.best_wordlist)) + ' out of ' + str(wordlist_length) 68 | 69 | def get_coords(self, word): 70 | """Return possible coordinates for each letter.""" 71 | word_length = len(word[0]) 72 | coordlist = [] 73 | temp_list = [(l, v) for l, letter in enumerate(word[0]) 74 | for k, v in self.let_coords.items() if k == letter] 75 | for coord in temp_list: 76 | letc = coord[0] 77 | for item in coord[1]: 78 | (rowc, colc, vertc) = item 79 | if vertc: 80 | if colc - letc >= 0 and (colc - letc) + word_length <= self.cols: 81 | row, col = (rowc, colc - letc) 82 | score = self.check_score_horiz(word, row, col, word_length) 83 | if score: 84 | coordlist.append([rowc, colc - letc, 0, score]) 85 | else: 86 | if rowc - letc >= 0 and (rowc - letc) + word_length <= self.rows: 87 | row, col = (rowc - letc, colc) 88 | score = self.check_score_vert(word, row, col, word_length) 89 | if score: 90 | coordlist.append([rowc - letc, colc, 1, score]) 91 | if coordlist: 92 | return max(coordlist, key=itemgetter(3)) 93 | else: 94 | return 95 | 96 | def first_word(self, word): 97 | """Place the first word at a random position in the grid.""" 98 | vertical = random.randrange(0, 2) 99 | if vertical: 100 | row = random.randrange(0, self.rows - len(word[0])) 101 | col = random.randrange(0, self.cols) 102 | else: 103 | row = random.randrange(0, self.rows) 104 | col = random.randrange(0, self.cols - len(word[0])) 105 | self.set_word(word, row, col, vertical) 106 | 107 | def add_words(self, word): 108 | """Add the rest of the words to the grid.""" 109 | coordlist = self.get_coords(word) 110 | if not coordlist: 111 | return 112 | row, col, vertical = coordlist[0], coordlist[1], coordlist[2] 113 | self.set_word(word, row, col, vertical) 114 | 115 | def check_score_horiz(self, word, row, col, word_length, score=1): 116 | cell_occupied = self.cell_occupied 117 | if col and cell_occupied(row, col-1) or col + word_length != self.cols and cell_occupied(row, col + word_length): 118 | return 0 119 | for letter in word[0]: 120 | active_cell = self.grid[row][col] 121 | if active_cell == self.empty: 122 | if row + 1 != self.rows and cell_occupied(row+1, col) or row and cell_occupied(row-1, col): 123 | return 0 124 | elif active_cell == letter: 125 | score += 1 126 | else: 127 | return 0 128 | col += 1 129 | return score 130 | 131 | def check_score_vert(self, word, row, col, word_length, score=1): 132 | cell_occupied = self.cell_occupied 133 | if row and cell_occupied(row-1, col) or row + word_length != self.rows and cell_occupied(row + word_length, col): 134 | return 0 135 | for letter in word[0]: 136 | active_cell = self.grid[row][col] 137 | if active_cell == self.empty: 138 | if col + 1 != self.cols and cell_occupied(row, col+1) or col and cell_occupied(row, col-1): 139 | return 0 140 | elif active_cell == letter: 141 | score += 1 142 | else: 143 | return 0 144 | row += 1 145 | return score 146 | 147 | def set_word(self, word, row, col, vertical): 148 | """Put words on the grid and add them to the word list.""" 149 | word.extend([row, col, vertical]) 150 | self.current_wordlist.append(word) 151 | 152 | horizontal = not vertical 153 | for letter in word[0]: 154 | self.grid[row][col] = letter 155 | if (row, col, horizontal) not in self.let_coords[letter]: 156 | self.let_coords[letter].append((row, col, vertical)) 157 | else: 158 | self.let_coords[letter].remove((row, col, horizontal)) 159 | if vertical: 160 | row += 1 161 | else: 162 | col += 1 163 | 164 | def cell_occupied(self, row, col): 165 | cell = self.grid[row][col] 166 | if cell == self.empty: 167 | return False 168 | else: 169 | return True 170 | 171 | class Exportfiles(object): 172 | def __init__(self, rows, cols, grid, wordlist, empty=' '): 173 | self.rows = rows 174 | self.cols = cols 175 | self.grid = grid 176 | self.wordlist = wordlist 177 | self.empty = empty 178 | 179 | def order_number_words(self): 180 | self.wordlist.sort(key=itemgetter(2, 3)) 181 | count, icount = 1, 1 182 | for word in self.wordlist: 183 | word.append(count) 184 | if icount < len(self.wordlist): 185 | if word[2] == self.wordlist[icount][2] and word[3] == self.wordlist[icount][3]: 186 | pass 187 | else: 188 | count += 1 189 | icount += 1 190 | 191 | def draw_img(self, name, context, px, xoffset, yoffset, RTL): 192 | for r in range(self.rows): 193 | for i, c in enumerate(self.grid[r]): 194 | if c != self.empty: 195 | context.set_line_width(1.0) 196 | context.set_source_rgb(0.5, 0.5, 0.5) 197 | context.rectangle(xoffset+(i*px), yoffset+(r*px), px, px) 198 | context.stroke() 199 | context.set_line_width(1.0) 200 | context.set_source_rgb(0, 0, 0) 201 | context.rectangle(xoffset+1+(i*px), yoffset+1+(r*px), px-2, px-2) 202 | context.stroke() 203 | if '_key.' in name: 204 | self.draw_letters(c, context, xoffset+(i*px)+10, yoffset+(r*px)+8, 'monospace 11') 205 | 206 | self.order_number_words() 207 | for word in self.wordlist: 208 | if RTL: 209 | x, y = ((self.cols-1)*px)+xoffset-(word[3]*px), yoffset+(word[2]*px) 210 | else: 211 | x, y = xoffset+(word[3]*px), yoffset+(word[2]*px) 212 | self.draw_letters(str(word[5]), context, x+3, y+2, 'monospace 6') 213 | 214 | def draw_letters(self, text, context, xval, yval, fontdesc): 215 | context.move_to(xval, yval) 216 | layout = PangoCairo.create_layout(context) 217 | font = Pango.FontDescription(fontdesc) 218 | layout.set_font_description(font) 219 | layout.set_text(text, -1) 220 | PangoCairo.update_layout(context, layout) 221 | PangoCairo.show_layout(context, layout) 222 | 223 | #def create_img(self, name, RTL): 224 | # px = 28 225 | # if name.endswith('png'): 226 | # surface = cairo.ImageSurface(cairo.FORMAT_RGB24, 10+(self.cols*px), 10+(self.rows*px)) 227 | # else: 228 | # surface = cairo.SVGSurface(name, 10+(self.cols*px), 10+(self.rows*px)) 229 | # context = cairo.Context(surface) 230 | # context.set_source_rgb(1, 1, 1) 231 | # context.rectangle(0, 0, 10+(self.cols*px), 10+(self.rows*px)) 232 | # context.fill() 233 | # self.draw_img(name, context, 28, 5, 5, RTL) 234 | # if name.endswith('png'): 235 | # surface.write_to_png(name) 236 | # else: 237 | # context.show_page() 238 | # surface.finish() 239 | 240 | #def export_pdf(self, xwname, filetype, lang, RTL, width=595, height=842): 241 | # px, xoffset, yoffset = 28, 36, 72 242 | # name = xwname + filetype 243 | # surface = cairo.PDFSurface(name, width, height) 244 | # context = cairo.Context(surface) 245 | # context.set_source_rgb(1, 1, 1) 246 | # context.rectangle(0, 0, width, height) 247 | # context.fill() 248 | # context.save() 249 | # sc_ratio = float(width-(xoffset*2))/(px*self.cols) 250 | # if self.cols <= 21: 251 | # sc_ratio, xoffset = 0.8, float(1.25*width-(px*self.cols))/2 252 | # context.scale(sc_ratio, sc_ratio) 253 | # self.draw_img(name, context, 28, xoffset, 80, RTL) 254 | # context.restore() 255 | # context.set_source_rgb(0, 0, 0) 256 | # self.draw_letters(xwname, context, round((width-len(xwname)*10)/2), yoffset/2, 'Sans 14 bold') 257 | # x, y = 36, yoffset+5+(self.rows*px*sc_ratio) 258 | # clues = self.wrap(self.legend(lang)) 259 | # self.draw_letters(lang[0], context, x, y, 'Sans 12 bold') 260 | # for line in clues.splitlines()[3:]: 261 | # if y >= height-(yoffset/2)-15: 262 | # context.show_page() 263 | # y = yoffset/2 264 | # if line.strip() == lang[1]: 265 | # if self.cols > 17 and y > 700: 266 | # context.show_page() 267 | # y = yoffset/2 268 | # y += 8 269 | # self.draw_letters(lang[1], context, x, y+15, 'Sans 12 bold') 270 | # y += 16 271 | # continue 272 | # self.draw_letters(line, context, x, y+18, 'Serif 9') 273 | # y += 16 274 | # context.show_page() 275 | # surface.finish() 276 | 277 | def create_files(self, name, save_format, lang, message): 278 | if Pango.find_base_dir(self.wordlist[0][0], -1) == Pango.Direction.RTL: 279 | [i.reverse() for i in self.grid] 280 | RTL = True 281 | else: 282 | RTL = False 283 | img_files = '' 284 | if 'p' in save_format: 285 | self.export_pdf(name, '_grid.pdf', lang, RTL) 286 | self.export_pdf(name, '_key.pdf', lang, RTL) 287 | img_files += name + '_grid.pdf ' + name + '_key.pdf ' 288 | if 'l' in save_format: 289 | self.export_pdf(name, 'l_grid.pdf', lang, RTL, 612, 792) 290 | self.export_pdf(name, 'l_key.pdf', lang, RTL, 612, 792) 291 | img_files += name + 'l_grid.pdf ' + name + 'l_key.pdf ' 292 | if 'n' in save_format: 293 | self.create_img(name + '_grid.png', RTL) 294 | self.create_img(name + '_key.png', RTL) 295 | img_files += name + '_grid.png ' + name + '_key.png ' 296 | if 's' in save_format: 297 | self.create_img(name + '_grid.svg', RTL) 298 | self.create_img(name + '_key.svg', RTL) 299 | img_files += name + '_grid.svg ' + name + '_key.svg ' 300 | if 'n' in save_format or 's' in save_format: 301 | self.clues_txt(name + '_clues.txt', lang) 302 | img_files += name + '_clues.txt' 303 | if message: 304 | print(message + img_files) 305 | 306 | def wrap(self, text, width=80): 307 | lines = [] 308 | for paragraph in text.split('\n'): 309 | line = [] 310 | len_line = 0 311 | for word in paragraph.split(): 312 | len_word = len(word) 313 | if len_line + len_word <= width: 314 | line.append(word) 315 | len_line += len_word + 1 316 | else: 317 | lines.append(' '.join(line)) 318 | line = [word] 319 | len_line = len_word + 1 320 | lines.append(' '.join(line)) 321 | return '\n'.join(lines) 322 | 323 | def word_bank(self): 324 | temp_list = list(self.wordlist) 325 | random.shuffle(temp_list) 326 | return 'Word bank\n' + ''.join([u'{}\n'.format(word[0]) for word in temp_list]) 327 | 328 | def legend(self, lang): 329 | outStrA, outStrD = u'\nClues\n{}\n'.format(lang[0]), u'{}\n'.format(lang[1]) 330 | for word in self.wordlist: 331 | if word[4]: 332 | outStrD += u'{:d}. {}\n'.format(word[5], word[1]) 333 | else: 334 | outStrA += u'{:d}. {}\n'.format(word[5], word[1]) 335 | return outStrA + outStrD 336 | 337 | def clues_txt(self, name, lang): 338 | with open(name, 'w') as clues_file: 339 | clues_file.write(self.word_bank()) 340 | clues_file.write(self.legend(lang)) 341 | -------------------------------------------------------------------------------- /genxword/complexstring.py: -------------------------------------------------------------------------------- 1 | # Authors: David Whitlock , Bryan Helmig 2 | # Crossword generator that outputs the grid and clues as a pdf file and/or 3 | # the grid in png/svg format with a text file containing the words and clues. 4 | # Copyright (C) 2010-2011 Bryan Helmig 5 | # Copyright (C) 2011-2016 David Whitlock 6 | # 7 | # Genxword is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # Genxword is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with genxword. If not, see . 19 | 20 | class ComplexString(str): 21 | """Handle accents and superscript / subscript characters.""" 22 | accents = [768, 769, 770, 771, 772, 773, 774, 775, 776, 777, 778, 779, 780, 781, 23 | 782, 783, 784, 785, 786, 787, 788, 789, 790, 791, 792, 793, 794, 795, 24 | 796, 797, 798, 799, 800, 801, 802, 803, 804, 805, 806, 807, 808, 809, 25 | 810, 811, 812, 813, 814, 815, 816, 817, 818, 819, 820, 821, 822, 823, 26 | 824, 825, 826, 827, 828, 829, 830, 831, 832, 833, 834, 835, 836, 837, 27 | 838, 839, 840, 841, 842, 843, 844, 845, 846, 847, 848, 849, 850, 851, 28 | 852, 853, 854, 855, 856, 857, 858, 859, 860, 861, 862, 863, 864, 865, 29 | 866, 867, 868, 869, 870, 871, 872, 873, 874, 875, 876, 877, 878, 879, 30 | 2306, 2366, 2367, 2368, 2369, 2370, 2371, 2372, 2375, 2376, 2379, 31 | 2380, 2402, 2403, 2433, 2492, 2494, 2495, 2496, 2497, 2498, 2499, 32 | 2500, 2503, 2504, 2507, 2508, 2519, 2530, 2531, 3006, 3007, 3008, 33 | 3009, 3010, 3014, 3015, 3016, 3018, 3019, 3020, 3021, 3031, 3633, 34 | 3636, 3637, 3638, 3639, 3640, 3641, 3655, 3656, 3657, 3658, 3659, 35 | 3660, 3661, 3662, 4139, 4140, 4141, 4142, 4143, 4144, 4145, 4146, 36 | 4150, 4151, 4152, 4154, 4155, 4156, 4157, 4158, 4182, 4185] 37 | 38 | special_chars = [2381, 2509, 4153] 39 | 40 | @staticmethod 41 | def _check_special(word, special): 42 | special_char = False 43 | formatted = [] 44 | for letter in word: 45 | if letter in special or special_char: 46 | special_char = not special_char 47 | formatted[-1] += letter 48 | continue 49 | formatted.append(letter) 50 | return formatted 51 | 52 | @staticmethod 53 | def format_word(word): 54 | """Join the accent to the character it modifies. 55 | This guarantees that the character is correctly displayed when 56 | iterating through the string, and that the length is correct. 57 | """ 58 | chars = {chr(n) for n in ComplexString.accents} 59 | special = {chr(n) for n in ComplexString.special_chars} 60 | formatted = [] 61 | for letter in word: 62 | if letter in chars: 63 | formatted[-1] += letter 64 | continue 65 | formatted.append(letter) 66 | if special.intersection(word): 67 | return ComplexString._check_special(formatted, special) 68 | return formatted 69 | 70 | def __new__(cls, content): 71 | cs = super().__new__(cls, content) 72 | cs.blocks = cls.format_word(content) 73 | return cs 74 | 75 | def __iter__(self): 76 | for block in self.blocks: 77 | yield block 78 | 79 | def __len__(self): 80 | return len(self.blocks) 81 | -------------------------------------------------------------------------------- /genxword/complexstring2.py: -------------------------------------------------------------------------------- 1 | # Authors: David Whitlock , Bryan Helmig 2 | # Crossword generator that outputs the grid and clues as a pdf file and/or 3 | # the grid in png/svg format with a text file containing the words and clues. 4 | # Copyright (C) 2010-2011 Bryan Helmig 5 | # Copyright (C) 2011-2016 David Whitlock 6 | # 7 | # Genxword is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # Genxword is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with genxword. If not, see . 19 | 20 | class ComplexString(unicode): 21 | """Handle accents and superscript / subscript characters.""" 22 | accents = [768, 769, 770, 771, 772, 773, 774, 775, 776, 777, 778, 779, 780, 781, 23 | 782, 783, 784, 785, 786, 787, 788, 789, 790, 791, 792, 793, 794, 795, 24 | 796, 797, 798, 799, 800, 801, 802, 803, 804, 805, 806, 807, 808, 809, 25 | 810, 811, 812, 813, 814, 815, 816, 817, 818, 819, 820, 821, 822, 823, 26 | 824, 825, 826, 827, 828, 829, 830, 831, 832, 833, 834, 835, 836, 837, 27 | 838, 839, 840, 841, 842, 843, 844, 845, 846, 847, 848, 849, 850, 851, 28 | 852, 853, 854, 855, 856, 857, 858, 859, 860, 861, 862, 863, 864, 865, 29 | 866, 867, 868, 869, 870, 871, 872, 873, 874, 875, 876, 877, 878, 879, 30 | 2306, 2366, 2367, 2368, 2369, 2370, 2371, 2372, 2375, 2376, 2379, 31 | 2380, 2402, 2403, 2433, 2492, 2494, 2495, 2496, 2497, 2498, 2499, 32 | 2500, 2503, 2504, 2507, 2508, 2519, 2530, 2531, 3006, 3007, 3008, 33 | 3009, 3010, 3014, 3015, 3016, 3018, 3019, 3020, 3021, 3031, 3633, 34 | 3636, 3637, 3638, 3639, 3640, 3641, 3655, 3656, 3657, 3658, 3659, 35 | 3660, 3661, 3662, 4139, 4140, 4141, 4142, 4143, 4144, 4145, 4146, 36 | 4150, 4151, 4152, 4154, 4155, 4156, 4157, 4158, 4182, 4185] 37 | 38 | special_chars = [2381, 2509, 4153] 39 | 40 | @staticmethod 41 | def _check_special(word, special): 42 | special_char = False 43 | formatted = [] 44 | for letter in word: 45 | if letter in special or special_char: 46 | special_char = not special_char 47 | formatted[-1] += letter 48 | continue 49 | formatted.append(letter) 50 | return formatted 51 | 52 | @staticmethod 53 | def format_word(word): 54 | """Join the accent to the character it modifies. 55 | This guarantees that the character is correctly displayed when 56 | iterating through the string, and that the length is correct. 57 | """ 58 | chars = {unichr(n) for n in ComplexString.accents} 59 | special = {unichr(n) for n in ComplexString.special_chars} 60 | formatted = [] 61 | for letter in word: 62 | if letter in chars: 63 | formatted[-1] += letter 64 | continue 65 | formatted.append(letter) 66 | if special.intersection(word): 67 | return ComplexString._check_special(formatted, special) 68 | return formatted 69 | 70 | def __new__(cls, content): 71 | cs = super(ComplexString, cls).__new__(cls, content) 72 | cs.blocks = cls.format_word(content) 73 | return cs 74 | 75 | def __iter__(self): 76 | for block in self.blocks: 77 | yield block 78 | 79 | def __len__(self): 80 | return len(self.blocks) 81 | -------------------------------------------------------------------------------- /genxword/control.py: -------------------------------------------------------------------------------- 1 | # Authors: David Whitlock , Bryan Helmig 2 | # Crossword generator that outputs the grid and clues as a pdf file and/or 3 | # the grid in png/svg format with a text file containing the words and clues. 4 | # Copyright (C) 2010-2011 Bryan Helmig 5 | # Copyright (C) 2011-2016 David Whitlock 6 | # 7 | # Genxword is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # Genxword is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with genxword. If not, see . 19 | 20 | import os 21 | import sys 22 | import gettext 23 | import random 24 | from .calculate import Crossword, Exportfiles 25 | 26 | PY2 = sys.version_info[0] == 2 27 | if PY2: 28 | from .complexstring2 import ComplexString 29 | input = raw_input 30 | else: 31 | from .complexstring import ComplexString 32 | 33 | base_dir = os.path.abspath(os.path.dirname(__file__)) 34 | d = '/usr/local/share' if 'local' in base_dir.split('/') else '/usr/share' 35 | gettext.bindtextdomain('genxword', os.path.join(d, 'locale')) 36 | if PY2: 37 | gettext.bind_textdomain_codeset('genxword', codeset='utf-8') 38 | gettext.textdomain('genxword') 39 | _ = gettext.gettext 40 | 41 | class Genxword(object): 42 | def __init__(self, auto=False, mixmode=False): 43 | self.auto = auto 44 | self.mixmode = mixmode 45 | 46 | def wlist(self, words, nwords=50): 47 | """Create a list of words and clues.""" 48 | wordlist = [line.strip().split(' ', 1) for line in words if line.strip()] 49 | if len(wordlist) > nwords: 50 | wordlist = random.sample(wordlist, nwords) 51 | self.wordlist = [[ComplexString(line[0].upper()), line[-1]] for line in wordlist] 52 | self.wordlist.sort(key=lambda i: len(i[0]), reverse=True) 53 | if self.mixmode: 54 | for line in self.wordlist: 55 | line[1] = self.word_mixer(line[0].lower()) 56 | 57 | def word_mixer(self, word): 58 | """Create anagrams for the clues.""" 59 | word = orig_word = list(word) 60 | for i in range(3): 61 | random.shuffle(word) 62 | if word != orig_word: 63 | break 64 | return ''.join(word) 65 | 66 | def grid_size(self, gtkmode=False): 67 | """Calculate the default grid size.""" 68 | if len(self.wordlist) <= 20: 69 | self.nrow = self.ncol = 17 70 | elif len(self.wordlist) <= 100: 71 | self.nrow = self.ncol = int((round((len(self.wordlist) - 20) / 8.0) * 2) + 19) 72 | else: 73 | self.nrow = self.ncol = 41 74 | if min(self.nrow, self.ncol) <= len(self.wordlist[0][0]): 75 | self.nrow = self.ncol = len(self.wordlist[0][0]) + 2 76 | if not gtkmode and not self.auto: 77 | gsize = str(self.nrow) + ', ' + str(self.ncol) 78 | grid_size = input(_('Enter grid size (') + gsize + _(' is the default): ')) 79 | if grid_size: 80 | self.check_grid_size(grid_size) 81 | 82 | def check_grid_size(self, grid_size): 83 | try: 84 | nrow, ncol = int(grid_size.split(',')[0]), int(grid_size.split(',')[1]) 85 | except: 86 | pass 87 | else: 88 | if len(self.wordlist[0][0]) < min(nrow, ncol): 89 | self.nrow, self.ncol = nrow, ncol 90 | 91 | def gengrid(self, name, saveformat): 92 | i = 0 93 | while 1: 94 | print(_('Calculating your crossword...')) 95 | calc = Crossword(self.nrow, self.ncol, '-', self.wordlist) 96 | print(calc.compute_crossword()) 97 | if self.auto: 98 | if float(len(calc.best_wordlist))/len(self.wordlist) < 0.9 and i < 5: 99 | self.nrow += 2; self.ncol += 2 100 | i += 1 101 | else: 102 | break 103 | else: 104 | h = input(_('Are you happy with this solution? [Y/n] ')) 105 | if h.strip() != _('n'): 106 | break 107 | inc_gsize = input(_('And increase the grid size? [Y/n] ')) 108 | if inc_gsize.strip() != _('n'): 109 | self.nrow += 2;self.ncol += 2 110 | lang = _('Across/Down').split('/') 111 | message = _('The following files have been saved to your current working directory:\n') 112 | exp = Exportfiles(self.nrow, self.ncol, calc.best_grid, calc.best_wordlist, '-') 113 | exp.create_files(name, saveformat, lang, message) 114 | -------------------------------------------------------------------------------- /msvcp90.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengshulin/exercise_generator/27fb3863920acab26484ab533aefa5d850201ee8/msvcp90.dll -------------------------------------------------------------------------------- /pyperclip/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Pyperclip 3 | 4 | A cross-platform clipboard module for Python. (only handles plain text for now) 5 | By Al Sweigart al@inventwithpython.com 6 | BSD License 7 | 8 | Usage: 9 | import pyperclip 10 | pyperclip.copy('The text to be copied to the clipboard.') 11 | spam = pyperclip.paste() 12 | 13 | if not pyperclip.copy: 14 | print("Copy functionality unavailable!") 15 | 16 | On Windows, no additional modules are needed. 17 | On Mac, the module uses pbcopy and pbpaste, which should come with the os. 18 | On Linux, install xclip or xsel via package manager. For example, in Debian: 19 | sudo apt-get install xclip 20 | 21 | Otherwise on Linux, you will need the gtk or PyQt4 modules installed. 22 | 23 | gtk and PyQt4 modules are not available for Python 3, 24 | and this module does not work with PyGObject yet. 25 | """ 26 | __version__ = '1.5.27' 27 | 28 | import platform 29 | import os 30 | import subprocess 31 | from .clipboards import (init_osx_clipboard, 32 | init_gtk_clipboard, init_qt_clipboard, 33 | init_xclip_clipboard, init_xsel_clipboard, 34 | init_klipper_clipboard, init_no_clipboard) 35 | from .windows import init_windows_clipboard 36 | 37 | # `import PyQt4` sys.exit()s if DISPLAY is not in the environment. 38 | # Thus, we need to detect the presence of $DISPLAY manually 39 | # and not load PyQt4 if it is absent. 40 | HAS_DISPLAY = os.getenv("DISPLAY", False) 41 | CHECK_CMD = "where" if platform.system() == "Windows" else "which" 42 | 43 | 44 | def _executable_exists(name): 45 | return subprocess.call([CHECK_CMD, name], 46 | stdout=subprocess.PIPE, stderr=subprocess.PIPE) == 0 47 | 48 | 49 | def determine_clipboard(): 50 | # Determine the OS/platform and set 51 | # the copy() and paste() functions accordingly. 52 | if 'cygwin' in platform.system().lower(): 53 | # FIXME: pyperclip currently does not support Cygwin, 54 | # see https://github.com/asweigart/pyperclip/issues/55 55 | pass 56 | elif os.name == 'nt' or platform.system() == 'Windows': 57 | return init_windows_clipboard() 58 | if os.name == 'mac' or platform.system() == 'Darwin': 59 | return init_osx_clipboard() 60 | if HAS_DISPLAY: 61 | # Determine which command/module is installed, if any. 62 | try: 63 | import gtk # check if gtk is installed 64 | except ImportError: 65 | pass 66 | else: 67 | return init_gtk_clipboard() 68 | 69 | try: 70 | import PyQt4 # check if PyQt4 is installed 71 | except ImportError: 72 | pass 73 | else: 74 | return init_qt_clipboard() 75 | 76 | if _executable_exists("xclip"): 77 | return init_xclip_clipboard() 78 | if _executable_exists("xsel"): 79 | return init_xsel_clipboard() 80 | if _executable_exists("klipper") and _executable_exists("qdbus"): 81 | return init_klipper_clipboard() 82 | 83 | return init_no_clipboard() 84 | 85 | 86 | def set_clipboard(clipboard): 87 | global copy, paste 88 | 89 | clipboard_types = {'osx': init_osx_clipboard, 90 | 'gtk': init_gtk_clipboard, 91 | 'qt': init_qt_clipboard, 92 | 'xclip': init_xclip_clipboard, 93 | 'xsel': init_xsel_clipboard, 94 | 'klipper': init_klipper_clipboard, 95 | 'windows': init_windows_clipboard, 96 | 'no': init_no_clipboard} 97 | 98 | copy, paste = clipboard_types[clipboard]() 99 | 100 | 101 | copy, paste = determine_clipboard() 102 | 103 | __all__ = ["copy", "paste"] 104 | -------------------------------------------------------------------------------- /pyperclip/clipboards.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import subprocess 3 | from .exceptions import PyperclipException 4 | 5 | EXCEPT_MSG = """ 6 | Pyperclip could not find a copy/paste mechanism for your system. 7 | For more information, please visit https://pyperclip.readthedocs.org """ 8 | PY2 = sys.version_info[0] == 2 9 | text_type = unicode if PY2 else str 10 | 11 | 12 | def init_osx_clipboard(): 13 | def copy_osx(text): 14 | p = subprocess.Popen(['pbcopy', 'w'], 15 | stdin=subprocess.PIPE, close_fds=True) 16 | p.communicate(input=text.encode('utf-8')) 17 | 18 | def paste_osx(): 19 | p = subprocess.Popen(['pbpaste', 'r'], 20 | stdout=subprocess.PIPE, close_fds=True) 21 | stdout, stderr = p.communicate() 22 | return stdout.decode('utf-8') 23 | 24 | return copy_osx, paste_osx 25 | 26 | 27 | def init_gtk_clipboard(): 28 | import gtk 29 | 30 | def copy_gtk(text): 31 | global cb 32 | cb = gtk.Clipboard() 33 | cb.set_text(text) 34 | cb.store() 35 | 36 | def paste_gtk(): 37 | clipboardContents = gtk.Clipboard().wait_for_text() 38 | # for python 2, returns None if the clipboard is blank. 39 | if clipboardContents is None: 40 | return '' 41 | else: 42 | return clipboardContents 43 | 44 | return copy_gtk, paste_gtk 45 | 46 | 47 | def init_qt_clipboard(): 48 | # $DISPLAY should exist 49 | from PyQt4.QtGui import QApplication 50 | 51 | app = QApplication([]) 52 | 53 | def copy_qt(text): 54 | cb = app.clipboard() 55 | cb.setText(text) 56 | 57 | def paste_qt(): 58 | cb = app.clipboard() 59 | return text_type(cb.text()) 60 | 61 | return copy_qt, paste_qt 62 | 63 | 64 | def init_xclip_clipboard(): 65 | def copy_xclip(text): 66 | p = subprocess.Popen(['xclip', '-selection', 'c'], 67 | stdin=subprocess.PIPE, close_fds=True) 68 | p.communicate(input=text.encode('utf-8')) 69 | 70 | def paste_xclip(): 71 | p = subprocess.Popen(['xclip', '-selection', 'c', '-o'], 72 | stdout=subprocess.PIPE, close_fds=True) 73 | stdout, stderr = p.communicate() 74 | return stdout.decode('utf-8') 75 | 76 | return copy_xclip, paste_xclip 77 | 78 | 79 | def init_xsel_clipboard(): 80 | def copy_xsel(text): 81 | p = subprocess.Popen(['xsel', '-b', '-i'], 82 | stdin=subprocess.PIPE, close_fds=True) 83 | p.communicate(input=text.encode('utf-8')) 84 | 85 | def paste_xsel(): 86 | p = subprocess.Popen(['xsel', '-b', '-o'], 87 | stdout=subprocess.PIPE, close_fds=True) 88 | stdout, stderr = p.communicate() 89 | return stdout.decode('utf-8') 90 | 91 | return copy_xsel, paste_xsel 92 | 93 | 94 | def init_klipper_clipboard(): 95 | def copy_klipper(text): 96 | p = subprocess.Popen( 97 | ['qdbus', 'org.kde.klipper', '/klipper', 'setClipboardContents', 98 | text.encode('utf-8')], 99 | stdin=subprocess.PIPE, close_fds=True) 100 | p.communicate(input=None) 101 | 102 | def paste_klipper(): 103 | p = subprocess.Popen( 104 | ['qdbus', 'org.kde.klipper', '/klipper', 'getClipboardContents'], 105 | stdout=subprocess.PIPE, close_fds=True) 106 | stdout, stderr = p.communicate() 107 | 108 | # Workaround for https://bugs.kde.org/show_bug.cgi?id=342874 109 | # TODO: https://github.com/asweigart/pyperclip/issues/43 110 | clipboardContents = stdout.decode('utf-8') 111 | # even if blank, Klipper will append a newline at the end 112 | assert len(clipboardContents) > 0 113 | # make sure that newline is there 114 | assert clipboardContents.endswith('\n') 115 | if clipboardContents.endswith('\n'): 116 | clipboardContents = clipboardContents[:-1] 117 | return clipboardContents 118 | 119 | return copy_klipper, paste_klipper 120 | 121 | 122 | def init_no_clipboard(): 123 | class ClipboardUnavailable(object): 124 | def __call__(self, *args, **kwargs): 125 | raise PyperclipException(EXCEPT_MSG) 126 | 127 | if PY2: 128 | def __nonzero__(self): 129 | return False 130 | else: 131 | def __bool__(self): 132 | return False 133 | 134 | return ClipboardUnavailable(), ClipboardUnavailable() 135 | -------------------------------------------------------------------------------- /pyperclip/exceptions.py: -------------------------------------------------------------------------------- 1 | import ctypes 2 | 3 | 4 | class PyperclipException(RuntimeError): 5 | pass 6 | 7 | 8 | class PyperclipWindowsException(PyperclipException): 9 | def __init__(self, message): 10 | message += " (%s)" % ctypes.WinError() 11 | super(PyperclipWindowsException, self).__init__(message) 12 | -------------------------------------------------------------------------------- /pyperclip/windows.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module implements clipboard handling on Windows using ctypes. 3 | """ 4 | import time 5 | import contextlib 6 | import ctypes 7 | from ctypes import c_size_t, sizeof, c_wchar_p, get_errno, c_wchar 8 | from .exceptions import PyperclipWindowsException 9 | 10 | 11 | class CheckedCall(object): 12 | def __init__(self, f): 13 | super(CheckedCall, self).__setattr__("f", f) 14 | 15 | def __call__(self, *args): 16 | ret = self.f(*args) 17 | if not ret and get_errno(): 18 | raise PyperclipWindowsException("Error calling " + self.f.__name__) 19 | return ret 20 | 21 | def __setattr__(self, key, value): 22 | setattr(self.f, key, value) 23 | 24 | 25 | def init_windows_clipboard(): 26 | from ctypes.wintypes import (HGLOBAL, LPVOID, DWORD, LPCSTR, INT, HWND, 27 | HINSTANCE, HMENU, BOOL, UINT, HANDLE) 28 | 29 | windll = ctypes.windll 30 | 31 | safeCreateWindowExA = CheckedCall(windll.user32.CreateWindowExA) 32 | safeCreateWindowExA.argtypes = [DWORD, LPCSTR, LPCSTR, DWORD, INT, INT, 33 | INT, INT, HWND, HMENU, HINSTANCE, LPVOID] 34 | safeCreateWindowExA.restype = HWND 35 | 36 | safeDestroyWindow = CheckedCall(windll.user32.DestroyWindow) 37 | safeDestroyWindow.argtypes = [HWND] 38 | safeDestroyWindow.restype = BOOL 39 | 40 | OpenClipboard = windll.user32.OpenClipboard 41 | OpenClipboard.argtypes = [HWND] 42 | OpenClipboard.restype = BOOL 43 | 44 | safeCloseClipboard = CheckedCall(windll.user32.CloseClipboard) 45 | safeCloseClipboard.argtypes = [] 46 | safeCloseClipboard.restype = BOOL 47 | 48 | safeEmptyClipboard = CheckedCall(windll.user32.EmptyClipboard) 49 | safeEmptyClipboard.argtypes = [] 50 | safeEmptyClipboard.restype = BOOL 51 | 52 | safeGetClipboardData = CheckedCall(windll.user32.GetClipboardData) 53 | safeGetClipboardData.argtypes = [UINT] 54 | safeGetClipboardData.restype = HANDLE 55 | 56 | safeSetClipboardData = CheckedCall(windll.user32.SetClipboardData) 57 | safeSetClipboardData.argtypes = [UINT, HANDLE] 58 | safeSetClipboardData.restype = HANDLE 59 | 60 | safeGlobalAlloc = CheckedCall(windll.kernel32.GlobalAlloc) 61 | safeGlobalAlloc.argtypes = [UINT, c_size_t] 62 | safeGlobalAlloc.restype = HGLOBAL 63 | 64 | safeGlobalLock = CheckedCall(windll.kernel32.GlobalLock) 65 | safeGlobalLock.argtypes = [HGLOBAL] 66 | safeGlobalLock.restype = LPVOID 67 | 68 | safeGlobalUnlock = CheckedCall(windll.kernel32.GlobalUnlock) 69 | safeGlobalUnlock.argtypes = [HGLOBAL] 70 | safeGlobalUnlock.restype = BOOL 71 | 72 | GMEM_MOVEABLE = 0x0002 73 | CF_UNICODETEXT = 13 74 | 75 | @contextlib.contextmanager 76 | def window(): 77 | """ 78 | Context that provides a valid Windows hwnd. 79 | """ 80 | # we really just need the hwnd, so setting "STATIC" 81 | # as predefined lpClass is just fine. 82 | hwnd = safeCreateWindowExA(0, b"STATIC", None, 0, 0, 0, 0, 0, 83 | None, None, None, None) 84 | try: 85 | yield hwnd 86 | finally: 87 | safeDestroyWindow(hwnd) 88 | 89 | @contextlib.contextmanager 90 | def clipboard(hwnd): 91 | """ 92 | Context manager that opens the clipboard and prevents 93 | other applications from modifying the clipboard content. 94 | """ 95 | # We may not get the clipboard handle immediately because 96 | # some other application is accessing it (?) 97 | # We try for at least 500ms to get the clipboard. 98 | t = time.time() + 0.5 99 | success = False 100 | while time.time() < t: 101 | success = OpenClipboard(hwnd) 102 | if success: 103 | break 104 | time.sleep(0.01) 105 | if not success: 106 | raise PyperclipWindowsException("Error calling OpenClipboard") 107 | 108 | try: 109 | yield 110 | finally: 111 | safeCloseClipboard() 112 | 113 | def copy_windows(text): 114 | # This function is heavily based on 115 | # http://msdn.com/ms649016#_win32_Copying_Information_to_the_Clipboard 116 | with window() as hwnd: 117 | # http://msdn.com/ms649048 118 | # If an application calls OpenClipboard with hwnd set to NULL, 119 | # EmptyClipboard sets the clipboard owner to NULL; 120 | # this causes SetClipboardData to fail. 121 | # => We need a valid hwnd to copy something. 122 | with clipboard(hwnd): 123 | safeEmptyClipboard() 124 | 125 | if text: 126 | # http://msdn.com/ms649051 127 | # If the hMem parameter identifies a memory object, 128 | # the object must have been allocated using the 129 | # function with the GMEM_MOVEABLE flag. 130 | count = len(text) + 1 131 | handle = safeGlobalAlloc(GMEM_MOVEABLE, 132 | count * sizeof(c_wchar)) 133 | locked_handle = safeGlobalLock(handle) 134 | 135 | ctypes.memmove(c_wchar_p(locked_handle), c_wchar_p(text), count * sizeof(c_wchar)) 136 | 137 | safeGlobalUnlock(handle) 138 | safeSetClipboardData(CF_UNICODETEXT, handle) 139 | 140 | def paste_windows(): 141 | with clipboard(None): 142 | handle = safeGetClipboardData(CF_UNICODETEXT) 143 | if not handle: 144 | # GetClipboardData may return NULL with errno == NO_ERROR 145 | # if the clipboard is empty. 146 | # (Also, it may return a handle to an empty buffer, 147 | # but technically that's not empty) 148 | return "" 149 | return c_wchar_p(handle).value 150 | 151 | return copy_windows, paste_windows 152 | -------------------------------------------------------------------------------- /setup_ExerciseGeneratorApp.py: -------------------------------------------------------------------------------- 1 | #coding:utf-8 2 | from distutils.core import setup 3 | import py2exe 4 | 5 | setup( 6 | name=u'Exercise Generator', 7 | version='1.5', 8 | description='automatically generate exercises', 9 | author='Peng Shulin', 10 | windows = [ 11 | { 12 | "script": "ExerciseGeneratorApp.py", 13 | } 14 | ], 15 | options = { 16 | "py2exe": { 17 | "compressed": 1, 18 | "optimize": 2, 19 | "dist_dir": "dist", 20 | } 21 | }, 22 | ) 23 | -------------------------------------------------------------------------------- /snapshots/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengshulin/exercise_generator/27fb3863920acab26484ab533aefa5d850201ee8/snapshots/demo.png -------------------------------------------------------------------------------- /snapshots/win7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengshulin/exercise_generator/27fb3863920acab26484ab533aefa5d850201ee8/snapshots/win7.png -------------------------------------------------------------------------------- /snapshots/winxp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengshulin/exercise_generator/27fb3863920acab26484ab533aefa5d850201ee8/snapshots/winxp.png -------------------------------------------------------------------------------- /snapshots/xubuntu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengshulin/exercise_generator/27fb3863920acab26484ab533aefa5d850201ee8/snapshots/xubuntu.png -------------------------------------------------------------------------------- /sudokulib/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.6a' 2 | 3 | -------------------------------------------------------------------------------- /sudokulib/decorators.py: -------------------------------------------------------------------------------- 1 | """Decorators used in Sudoku""" 2 | 3 | def check_length(func): 4 | """Ensures that the provided list of values has a valid length""" 5 | 6 | def wrapped(self, num, values): 7 | if len(values) == self.side_length: 8 | return func(self, num, values) 9 | else: 10 | raise ValueError('Invalid values. Please specify a list of %i values.' % self.side_length) 11 | return wrapped 12 | 13 | def handle_negative(func): 14 | """Handles negative values for get_row and get_col""" 15 | 16 | def wrapped(self, num): 17 | if num < 0: 18 | num = self.side_length + num 19 | return func(self, num) 20 | return wrapped 21 | 22 | def requires_solution(func): 23 | """Solves the puzzle before returning""" 24 | 25 | def wrapped(self, *args, **kwargs): 26 | if None in self.solution: 27 | self.solve() 28 | 29 | return func(self, *args, **kwargs) 30 | return wrapped 31 | 32 | -------------------------------------------------------------------------------- /sudokulib/jigsaw.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from decorators import requires_solution 4 | from sudoku import Sudoku 5 | 6 | class JigsawSudoku(Sudoku): 7 | VALID_SIZES = (3,) 8 | REGIONS = ( 9 | ( 0, 1, 2, 9,10,11,18,27,28), 10 | ( 3,12,13,14,23,24,25,34,35), 11 | ( 4, 5, 6, 7, 8,15,16,17,26), 12 | (19,20,21,22,29,36,37,38,39), 13 | (30,31,32,33,40,47,48,49,50), 14 | (41,42,43,44,51,58,59,60,61), 15 | (45,46,55,56,57,66,67,68,77), 16 | (52,53,62,69,70,71,78,79,80), 17 | (54,63,64,65,72,73,74,75,76), 18 | ) 19 | REGION_COLORS = ( 20 | (41, 30), (42, 30), (43, 30), 21 | (44, 30), (45, 30), (46, 30), 22 | (47, 30), (41, 30), (42, 30), 23 | ) 24 | 25 | def get_region(self, row, col): 26 | index = self.row_col_to_index(row, col) 27 | return self.get_region_by_index(index) 28 | 29 | def get_region_by_index(self, index): 30 | """Returns values used in the region at the specified index""" 31 | 32 | for region in JigsawSudoku.REGIONS: 33 | if index in region: 34 | return [self.solution[i] for i in region] 35 | 36 | raise ValueError('Invalid index') 37 | 38 | @requires_solution 39 | def print_grid(self, grid): 40 | """Prints a nicely formatted version of the Sudoku grid""" 41 | 42 | fmt = '\033[%s;%sm' 43 | norm = '\033[0m' 44 | field_width = len(str(self.side_length)) + 2 45 | 46 | for i, val in enumerate(grid): 47 | if i % 9 == 0 and i > 0: 48 | sys.stdout.write('\n') 49 | 50 | for rid, r in enumerate(self.REGIONS): 51 | if i in r: 52 | region_id = rid 53 | break 54 | 55 | col = fmt % self.REGION_COLORS[region_id] 56 | val = str(val).center(field_width) 57 | sys.stdout.write('%s%s%s' % (col, val, norm)) 58 | 59 | sys.stdout.write('\n') 60 | 61 | def main(): 62 | s = JigsawSudoku() 63 | s.print_masked() 64 | #print '=' * 50 65 | s.print_solution() 66 | 67 | #s.clear() 68 | #s.init_grid([3,0,0,0,0,0,0,0,4,0,0,2,0,6,0,1,0,0,0,1,0,9,0,8,0,2,0,0,0,5,0,0,0,6,0,0,0,2,0,0,0,0,0,1,0,0,0,9,0,0,0,8,0,0,0,8,0,3,0,4,0,6,0,0,0,4,0,1,0,9,0,0,5,0,0,0,0,0,0,0,7]) 69 | #s.print_masked() 70 | #print '=' * 50 71 | #s.print_solution() 72 | 73 | if __name__ == '__main__': 74 | main() 75 | -------------------------------------------------------------------------------- /sudokulib/sudoku.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | A simple backtracking Sudoku generator 6 | """ 7 | 8 | from copy import copy 9 | import math 10 | import random 11 | import time 12 | 13 | from decorators import handle_negative, check_length, requires_solution 14 | 15 | MIN = -1 16 | EASY = 0 17 | MEDIUM = 1 18 | HARD = 2 19 | EXPERT = 3 20 | INSANE = 4 21 | GOOD_LUCK = 5 22 | 23 | class Sudoku(object): 24 | VALID_SIZES = (2, 3, 4, 5) 25 | SCALE = { 26 | MIN: 0.65, 27 | EASY: 0.45, 28 | MEDIUM: 0.35, 29 | HARD: 0.30, 30 | EXPERT: 0.24, 31 | INSANE: 0.17, 32 | GOOD_LUCK: 0.12, 33 | } 34 | 35 | def __init__(self, grid_size=3, difficulty=MEDIUM): 36 | self.set_grid_size(grid_size) 37 | self.set_difficulty(difficulty) 38 | self.side_length = self.grid_size ** 2 39 | self.square = self.side_length ** 2 40 | self.possibles = set([i + 1 for i in range(self.side_length)]) 41 | self.solution = [] 42 | self._masked = None 43 | self.iterations = 0 44 | 45 | self.clear() 46 | 47 | def init_grid(self, values): 48 | """Initializes the grid with some existing values.""" 49 | 50 | validate = lambda v: (str(v).isdigit() and v > 0 and v <= self.side_length) and v or None 51 | if len(values) == self.square: 52 | # 1-dimensional list of values 53 | self.solution = [validate(v) for v in values] 54 | elif len(values) == self.side_length: 55 | # multi-dimensional list of values: [[row 1], [row 2], ..., [row n]] 56 | for i, row in enumerate(values): 57 | self.set_row(i, [validate(v) for v in row]) 58 | 59 | def clear(self): 60 | """Cleans up the Sudoku solution""" 61 | 62 | self.solution = [None for i in range(self.square)] 63 | 64 | def row_col_to_index(self, row, col): 65 | """Translates a (row, col) into an index in our 1-dimensional list""" 66 | 67 | return (row * self.side_length) + col 68 | 69 | def index_to_row_col(self, index): 70 | """Translates an index in our 1-dimensional list to a (row, col)""" 71 | 72 | return divmod(index, self.side_length) 73 | 74 | def set_grid_size(self, grid_size): 75 | """Sets the grid size""" 76 | 77 | if grid_size in self.VALID_SIZES: 78 | self.grid_size = grid_size 79 | else: 80 | raise ValueError('Invalid size. Options are: %s' % (self.VALID_SIZES, )) 81 | 82 | def set_difficulty(self, difficulty): 83 | """Sets the difficulty level for a masked grid""" 84 | 85 | valid_diff = self.SCALE.keys() 86 | valid_diff.remove(MIN) 87 | 88 | if difficulty in valid_diff: 89 | self.difficulty = difficulty 90 | else: 91 | raise ValueError('Invalid difficulty level. Options are: %s' % (valid_diff,)) 92 | 93 | @property 94 | @requires_solution 95 | def masked_grid(self): 96 | """Generates and caches a Sudoku with several squares hidden""" 97 | 98 | if self._masked is None: 99 | min = math.floor(Sudoku.SCALE[self.difficulty] * self.square) 100 | max = math.ceil(Sudoku.SCALE.get(self.difficulty - 1, min) * self.square) 101 | numbers_to_show = random.randint(min, max) 102 | 103 | self._masked = [True for i in range(numbers_to_show)] + \ 104 | ['_' for i in range(self.square - numbers_to_show)] 105 | random.shuffle(self._masked) 106 | for i, value in enumerate(self.solution): 107 | if self._masked[i] == True: 108 | self._masked[i] = value 109 | 110 | return self._masked 111 | 112 | @handle_negative 113 | def get_row(self, row): 114 | """Returns all values for the specified row""" 115 | 116 | start = row * self.side_length 117 | end = start + self.side_length 118 | return self.solution[start:end] 119 | 120 | def get_row_by_index(self, index): 121 | """Returns all values for the row at the given index""" 122 | 123 | row, col = self.index_to_row_col(index) 124 | return self.get_row(row) 125 | 126 | @check_length 127 | def set_row(self, row, values): 128 | """Sets the values for the specified row""" 129 | 130 | start = row * self.side_length 131 | end = start + self.side_length 132 | self.solution[start:end] = values 133 | 134 | @handle_negative 135 | def get_col(self, col): 136 | """Returns all values for the specified column""" 137 | 138 | return self.solution[col::self.side_length] 139 | 140 | def get_col_by_index(self, index): 141 | """ 142 | Returns all values for the column at the given index 143 | """ 144 | 145 | row, col = self.index_to_row_col(index) 146 | return self.get_col(col) 147 | 148 | @check_length 149 | def set_col(self, col, values): 150 | """Sets the values for the specified column""" 151 | 152 | self.solution[col::self.side_length] = values 153 | 154 | def get_region(self, row, col): 155 | """Returns all values for the region at the given (row, col)""" 156 | 157 | start_row = int(row / self.grid_size) * self.grid_size 158 | start_col = int(col / self.grid_size) * self.grid_size 159 | 160 | values = [] 161 | for i in range(self.grid_size): 162 | start = (start_row + i) * self.side_length + start_col 163 | end = start + self.grid_size 164 | values.extend(self.solution[start:end]) 165 | 166 | return values 167 | 168 | def get_region_by_index(self, index): 169 | """Returns all values for the region at the given index""" 170 | 171 | row, col = self.index_to_row_col(index) 172 | return self.get_region(row, col) 173 | 174 | def get_used(self, row, col): 175 | """Returns a list of all used values for a row, column, and region""" 176 | 177 | r = self.get_row(row) 178 | c = self.get_col(col) 179 | region = self.get_region(row, col) 180 | 181 | return (r + c + region) 182 | 183 | def get_used_by_index(self, index): 184 | """Returns a list of all used values for a row, col, and region""" 185 | 186 | row, col = self.index_to_row_col(index) 187 | return self.get_used(row, col) 188 | 189 | def is_valid_value(self, row, col, value): 190 | """ 191 | Validates whether or not a value will work in the grid, without using 192 | the pre-generated solution 193 | """ 194 | 195 | return value not in self.get_used(row, col) 196 | 197 | def is_valid_value_for_index(self, index, value): 198 | """Validates a value for the given index""" 199 | 200 | row, col = self.index_to_row_col(index) 201 | return self.is_valid_value(row, col, value) 202 | 203 | def fill_square(self, index=0): 204 | """ 205 | Recursively populates each square on the Sudoku grid until a solution 206 | is found. Most of this method was inspired by Jeremy Brown 207 | """ 208 | 209 | if self.solution[index]: 210 | if index + 1 >= self.square: 211 | return True 212 | return self.fill_square(index + 1) 213 | 214 | used = self.get_used_by_index(index) 215 | possible = list(self.possibles.difference(used)) 216 | if len(possible) == 0: 217 | return False 218 | 219 | #if self.iterations % 50000 == 0: print index, possible #, row, col, region 220 | random.shuffle(possible) 221 | 222 | for new_value in possible: 223 | self.solution[index] = new_value 224 | self.iterations += 1 225 | 226 | if index + 1 >= self.square or self.fill_square(index + 1): 227 | return True 228 | 229 | self.solution[index] = None 230 | 231 | return False 232 | 233 | def solve(self): 234 | self.iterations = 0 235 | self.fill_square(0) 236 | 237 | @requires_solution 238 | def print_grid(self, grid): 239 | """Prints a nicely formatted version of the Sudoku grid""" 240 | 241 | field_width = len(str(self.side_length)) + 2 242 | format = ''.join(['%s' for i in range(self.grid_size)]) 243 | format = '|'.join([format for i in range(self.grid_size)]) 244 | 245 | for row in range(self.side_length): 246 | start = row * self.side_length 247 | end = start + self.side_length 248 | values = tuple(str(val).center(field_width) for val in grid[start:end]) 249 | print format % values 250 | 251 | # print a dividing line for each set of regions 252 | if row < self.side_length - 1 and (row + 1) % self.grid_size == 0: 253 | print '+'.join('-' * field_width * self.grid_size for i in range(self.grid_size)) 254 | 255 | def print_solution(self): 256 | """Prints the generated solution nicely formatted""" 257 | 258 | return self.print_grid(self.solution) 259 | 260 | def print_masked(self): 261 | """Prints a masked version of the grid""" 262 | 263 | return self.print_grid(self.masked_grid) 264 | 265 | def main(): 266 | import sys 267 | try: 268 | size = int(sys.argv[1]) 269 | except IndexError: 270 | size = 3 271 | s = Sudoku(size, difficulty=EASY) 272 | s.print_solution() 273 | print '=' * (size ** 3 + size - 1) 274 | s.print_masked() 275 | 276 | if __name__ == '__main__': 277 | main() 278 | 279 | --------------------------------------------------------------------------------