1 (изменено: ypppu, 2017-03-26 14:34:14)

Тема: AHK: Cумма прописью (в любом окне)

Классический бухгалтерский функционал: По Ctrl+Shift+W добавляет к выделенной сумме в числовом виде строку, описывающую эту сумму прописью в рублях и копейках. Копейки можно выводить как прописью так и числом (с лидирующим нулём при необходимости).
Работает в окнах любых приложений (как локальных, так и опубликованных через Citrix). Проверен на Win7 и Win10.

; Сумма прописью
^+vk57:: ; {Ctrl Shift W}
tmp := ClipboardAll
Clipboard := ""
Send, ^{Ins}
ClipWait, , 1
ctext := Clipboard ; очищаем текст от форматирования
;Извлекаем число из выделенного текста, например: "ИТОГО: 2 431 234 012, 31 руб."
ctext := RegExReplace(ctext,"=|,", ".")
ctext := RegExReplace(ctext,"[^\d\.]")
ctext := RegExReplace(ctext,"\.$")
If (RegExMatch(ctext,"\d+")) {
	Clipboard := " (" . Sum2Words(ctext, 2) . ")"
	Send, {Right}
	Sleep 200
	Send, +{Ins}
	Sleep 200
}
Clipboard := tmp
Return

;Использован прототип (VBA) http://www.script-coding.com/SumInWords.zip
;Функция возвращает сумму прописью в указанной валюте.
;Первый аргумент - число (сумма).
;Второй аргумент - формат вывода копеек:
;1 - копейки числом,
;2 - копейки числом с лидирующим нулём при необходимости,
;любое другое число - копейки прописью.
Sum2Words(sum, kop) {
	;Округление до двух знаков после запятой
	sum := Round(Abs(sum), 2)
	words := []

	;Миллиард
	num := Floor(sum/1000000000)
	If (num > 0) {
		words.Push(Rest2Words(num, True))
		words.Push(["миллиардов","миллиарда","миллиард"][CaseOfWord(num)])
	}
	sum -= num*1000000000

	;Миллион
	num := Floor(sum/1000000)
	If (num > 0) {
		words.Push(Rest2Words(num, True))
		words.Push(["миллионов","миллиона","миллион"][CaseOfWord(num)])
	}
	sum -= num*1000000

	;Тысяча
	num := Floor(sum/1000)
	If (num > 0) {
		words.Push(Rest2Words(num, False))
		words.Push(["тысяч","тысячи","тысяча"][CaseOfWord(num)])
	}
	sum -= num*1000

	;Рублей
	num := Floor(sum)
	If (num > 0) {
		words.Push(Rest2Words(num, True))
		words.Push(["рублей","рубля","рубль"][CaseOfWord(num)])
	}
	sum -= num

	;Копеек
	num := Round(sum*100)
	If (kop = 1) { ; числом
		words.Push(num)
	}
	Else If (kop = 2) { ; числом с лидирующим 0
		StringRight, num, % "0" . num, 2
		words.Push(num)
	}
	Else { ; прописью
		words.Push(Rest2Words(num, False))
	}
	words.Push(["копеек","копейки","копейка"][CaseOfWord(num)])

	; Склеиваем все слова в одну строку
	words := Join(words, " ")

	;С заглавной буквы
	StringLeft, firstChar, words, 1
	StringUpper, firstChar, firstChar
	StringRight, otherChars, words, StrLen(words)-1

	Return firstChar . otherChars
}

;Функция возвращает индекс в массиве названий разряда числа.
;Аргумент - целое положительное число, меньшее тысячи.
CaseOfWord(num){
	StringRight, tens, num, 2    ;Десятки (два последних знака)
	If (tens < 11 Or tens > 19) {
		StringRight, unitys, num, 1  ;Единицы (один последний знак)
		If unitys = 1
			Return 3
		Else If unitys between 2 and 4
			Return 2
	}
	Return 1
}

;Функция возвращает число прописью.
;Первый аргумент - целое положительное число, меньшее тысячи.
;Второй аргумент - род числа (True - мужской, False - женский, для тысяч и копеек).
Rest2Words(rest, gender){
	rest := Abs(rest)
	If ((rest <= 0) Or (rest > 999)) {
		Return "ноль"
	}
	words := []

	;Сотни
	num := Floor(rest / 100)
	If (num > 0) {
		words.Push(["сто","двести","триста","четыреста","пятьсот","шестьсот","семьсот","восемьсот","девятьсот"][num])
	}
	rest -= num*100

	;Десятки
	num := Floor(rest/10)
	If (num > 1) {
		words.Push(["двадцать","тридцать","сорок","пятьдесят","шестьдесят","семьдесят","восемьдесят","девяносто"][num-1])
		rest -= num*10
	}
	Else If (num = 1) {
		words.Push(["десять","одиннадцать","двенадцать","тринадцать","четырнадцать","пятнадцать","шестнадцать","семнадцать","восемнадцать","девятнадцать"][rest-9])
		rest := 0
	}

	;Единицы
	If (rest > 0) {
		If (rest = 1) {
			words.Push(gender ? "один" : "одна")
		}
		Else If (rest = 2) {
			words.Push(gender ? "два" : "две")
		}
		Else {
			words.Push(["три","четыре","пять","шесть","семь","восемь","девять"][rest-2])
		}
	}

	Return Join(words, " ")
}

; Возвращает склеенный в одну строку массив Array (с использованием разделителя Sep)
Join(array, sep) {
	for k, v in array
		out .= sep . v
	Return SubStr(out, 1+StrLen(sep))
}

В AHK я - новичок. Не понимаю недоделанности некоторых общеупотребительных операторов, например, того же If. Конечно, привыкнуть к этому можно, но такие несуразности бросаются в глаза и создают не решаемые логическим путем глюки...
В общем, буду очень благодарен за советы и рекомендации по скрипту.
Интересует не сколько расширение функциональности, сколько правильность и корректность использованных функций, рефакторинг кода.

2All
Пожалуйста, обратите внимание на начальный маленький блок получения и вставки текста. Нет замечаний? Просто мне кажется это - единственно правильный подход. Все другие (в т.ч. WinAPI) не универсальны (не со всеми окнами работать будут).
Может быть кто придумает как определить нередактируемый текст. Ну чтобы если такое случается, результат вычислений не в текст вставлять, а в MsgBox выводить. Вариант с вырезанием выделенного текста в буфер (если прокатило - значит - редактируемый) и последующей его вставкой знаю, но не нравится, поскольку надо добавлять кучу доп.кода чтобы содержимое буфера обмена не перекорежило.

2 (изменено: ypppu, 2017-03-26 14:47:38)

Re: AHK: Cумма прописью (в любом окне)

mozers пишет:

как определить нередактируемый текст. Ну чтобы если такое случается, результат вычислений не в текст вставлять, а в MsgBox выводить.

Вместо Send, ^{Ins} что ли?
Кстати,

ctext := RegExReplace(ctext,"[^\d\.]")
ctext := RegExReplace(ctext,"\.$")
If (RegExMatch(ctext,"\d+")) {
	Clipboard := " (" . Sum2Words(ctext, 2) . ")"
	Send, {Right}
	Sleep 200
	Send, +{Ins}
	Sleep 200
}

зачем тут задержки? Надо бы SendInput использовать. Или так:

ctext := RegExReplace(ctext,"[^\d.]|\.$")
If ctext is number {
	SendInput, {Right}
	Control EditPaste, % ctext,, A
}

Ещё в начало кода стоит добавить для скорости:

#NoEnv
ListLines, Off
#KeyHistory, 0
SetBatchLines,-1

3 (изменено: stealzy, 2017-03-26 14:08:38)

Re: AHK: Cумма прописью (в любом окне)

в MsgBox выводить

Чтобы бухи вслух по слогам читали что-ли ?
Если кому и понадобится число прописью, то исключительно в редактируемом поле. В нем с текстом и работают.
По моей практике лучший метод отправки нажатий - Input, для сочетаний с Ctrl самый надежный - Play.

Hotkey % "+^" Format("vk{:02x}", GetKeyVK("W")), CopyConvertSum2WordsPaste
Return

CopyConvertSum2WordsPaste() {
	SendMode Input
	SetKeyDelay 20, 20
	tmpClip := ClipboardAll, Clipboard := ""
	SendPlay ^{Ins}
	ClipWait,.1,1
	If ErrorLevel
		Return
	ctext := Clipboard ; очищаем текст от форматирования
	;Извлекаем число из выделенного текста, например: "ИТОГО: 2 431 234 012, 31 руб."
	ctext := RegExReplace(ctext,"=|,", ".")
	ctext := RegExReplace(ctext,"[^\d\.]")
	ctext := RegExReplace(ctext,"\.$")
	If (RegExMatch(ctext,"\d+")) {
		Clipboard := " (" . Sum2Words(ctext, 2) . ")"
		Send {Right}
		SendPlay +{Ins}
	}
	Clipboard := tmpClip
}

4 (изменено: ypppu, 2017-03-26 14:54:13)

Re: AHK: Cумма прописью (в любом окне)

зачем тут задержки? Надо бы SendInput использовать.

Без задержек, курсор не успевает подвинуться Send,{Right} и убрать выделение и тогда вместо выделенного текста вставляется не результат преобразования, а предыдущее содержимое буфера обмена (это значит не успело сработать Send,+{Ins}). Стабильно такое происходит с "тормознутыми" окнами (например, с окнами удаленных опубликованных приложений).
SendInput,+{Ins} у меня почему то периодически "глючит" - выводит текст "+{Ins}" как будто я включил режим Raw => Я решил отказаться от использования этой "глюкавой" директивы.

Или так:

Спасибо! Буду тестировать.

stealzy

Чтобы бухи вслух по слогам читали что-ли lol?

Для этой поделки - действительно излишне. Просто хотелось использовать типовой механизм для всех подобных скриптов.

По моей практике лучший метод отправки нажатий - Input, для сочетаний с Ctrl самый надежный - Play.

Спасибо за подсказку и код! Буду тестировать.


+ открыть спойлер

Уважаемые модераторы!
Я крайне извиняюсь что моя невольно брошенная фраза разбила данный топик на 2 разных темы.

5 (изменено: stealzy, 2017-03-26 14:23:36)

Re: AHK: Cумма прописью (в любом окне)

mozers, именно так у меня и глючил SendInput, вероятно это касается любых сочетаний с модификаторами.
SetKeyDelay как раз, чтобы не писать Sleep`ы.
Добавил в код время и обработку ErrorLevel ClipWait - не зря же он там.

6

Re: AHK: Cумма прописью (в любом окне)

Обсуждение способов присваивания переменной переехало в соответствующую тему.

7

Re: AHK: Cумма прописью (в любом окне)

Тестирование затянулось... Вся проблема в том, что с локальными приложениями достаточно надежно работают все варианты. Когда дело доходит до опубликованных удаленных приложений, то работоспособность команд, эмулирующих клавиатурные нажатия становится точно такой же хреновой как и в других скриптовых языках. К сожалению, я не вижу способа обеспечить работу скрипта с самыми различными окнами без использования этих долбанных Send-ов. Поэтому, долгие эксперименты привели меня к такому коду (stealzy, я многое содрал у Вас):

; Сумма прописью {Ctrl Shift W}
Hotkey % "+^" Format("vk{:02x}", GetKeyVK("W")), CopyConvertPaste
Return

CopyConvertPaste(){
	SetKeyDelay 20, 20
	tmp := ClipboardAll
	Clipboard := ""
	Send, ^{Ins}
	ClipWait,.1,1
	If ErrorLevel
		Return
	ctext := Clipboard ; очищаем текст от форматирования
	;Извлекаем число из выделенного текста, например: "ИТОГО: 2 431 234 012, 31 руб."
	ctext := RegExReplace(ctext,"=|,", ".")
	ctext := RegExReplace(ctext,"[^\d\.]")
	ctext := RegExReplace(ctext,"\.$")
	If (RegExMatch(ctext,"\d+")) {
		Clipboard := " (" . Sum2Words(ctext, 2) . ")"
		Send, {Right}
		Sleep 400
		Send, +{Ins}
		Sleep 200
	}
	Clipboard := tmp
}

Сразу отвечу на вероятные возражения:
1. SetKeyDelay использование Sleep не отменяет. Проверено. Да и в доках, если внимательно читать, тоже написано.
2. SendPlay работает как и SendInput через пень колоду.
3. SendMode Input делает единственно рабочую команду Send такой же тупой как SendInput и SendPlay (что, в общем, так же написано в документации)

8

Re: AHK: Cумма прописью (в любом окне)

Одна десятая секунды ожидания наполнения буфера это нонсенс - всегда орлянка. Система занята не одними вашими нуждами. Поставьте ожидание полсекунды-секунду и с божьей помощью это будет работать.