Skip to content

Instantly share code, notes, and snippets.

@endolith
Last active March 15, 2017 04:08
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save endolith/127286 to your computer and use it in GitHub Desktop.
Save endolith/127286 to your computer and use it in GitHub Desktop.
pycalcy - unit calculator for Launchy

pycalcy - Unit calculator for Launchy

Fast, local unit calculator based on GNU Units (instead of Google Calc)

Needs pylaunchy and GNU Units installed first

Called by typing 'c' and pressing Tab. First argument is the expression to be evaluated. Second argument (optional) is the units to convert to. Ideally, you would press Tab after typing the expression, but I don't know how to do that yet, so type a greater-than sign '>' for now.

It uses units in verbose mode, so entering this:

c ► 1 inch + 3 feet > nanometers

produces this:

1 inch + 3 feet = 9.398e+008 nanometers

This functionality intersects with that of Google Calc, but they each have advantages and disadvantages.

Google Calc

  • Convert binary to decimal to octal to roman numerals
  • Type out constants in English like "speed of light"
  • Up-to-date currency conversions
  • Non-linear conversions are more intuitive, like "50 C in F"
  • Verbose output helps you notice mistakes, like "k" vs "K"
  • Has some more math functions, imaginary numbers, modulo
  • Knows the answer to life, the universe, and everything

GNU units

  • Many more units and prefixes included. Convert newtons to aeginamina-exaparsecs/siderealyear²
  • Don't need to be online, doesn't need to load a whole webpage in the background
  • Can type in abbreviated units like "mV" instead of "millivolt"
  • You can define your own units in a ~/.units.dat file, like I've done here: Custom unit definitions for GNU Units

In Windows, install in C:\Program Files\Launchy\Plugins\python

Needs to handle all these situations. Make them happen and get a useful result from each
errors from single-shot command is different from interactive session
more complex than just returning stripped version:
'conformability error: g = 0.001 kg and m = 1 m'
'reciprocal conversion: 1 / s = 1 Hz and 1 / s = (1 / 1) Hz'
Can't just do this for multiple lines or you will get
'g = 0.0022046226 lb: g = (1 / 453.59237) lb and'
* "Successful completion"
* "Product overflow"
* "Unit reduction error (bad unit definition)"
* "Bad argument"
* "Weird nonlinear unit type (bug in program)"
* "Argument of table outside domain"
* "Nonlinear unit definition has unit error"
* "No inverse defined"
* "Argument wrong dimension or bad nonlinear unit definition"
Conversion
W ' ft\n\tm = 3.2808399 ft\n\tm = (1 / 0.3048) ft\nYou have:'
L ' \tm = 3.2808399 ft\n\tm = (1 / 0.3048) ft\nYou have:'
Definition
W ' \n\tDefinition: kilo m = 1000 m\nYou have:'
L ' \tDefinition: kilo m = 1000 m\nYou have:'
Nothing
W ' \nYou have:'
L ' You have:'
Conformability (m to W)
W ' W\nconformability error\n\tm = 1 m\n\tW = 1 kg m^2 / s^3\nYou have:'
L ' conformability error\n\tm = 1 m\n\tW = 1 kg m^2 / s^3\nYou have:'
Unknown unit (q) at You have
W " q\nUnknown unit 'q'\nYou have:"
L " Unknown unit 'q'\nYou have:"
Unknown unit (q) at You want
W " q\nUnknown unit 'q'\nYou want: \n\tDefinition: 1 m\nYou have:"
L " Unknown unit 'q'\nYou want: \tDefinition: 1 m\nYou have:"
Wrong dimension to function pH(W)
W ' pH(W)\n ^\nFunction argument has wrong dimension\nYou have:'
L ' ^\nFunction argument has wrong dimension\nYou have:'
Parse error
W ' (\n ^\nParse error\nYou have:'
L ' ^\nParse error\nYou have:'
Reciprocal conversion
W ' Hz\n\treciprocal conversion\n\t1 / s = 1 Hz\n\t1 / s = (1 / 1) Hz\nYou have:'
L ' \treciprocal conversion\n\t1 / s = 1 Hz\n\t1 / s = (1 / 1) Hz\nYou have:'
Conformability sum
W ' 1 + m\n ^\nIllegal sum or difference of non-conformable units\nYou have:'
L ' ^\nIllegal sum or difference of non-conformable units\nYou have:'
Not a root
W ' sqrt(m)\n ^\nUnit not a root\nYou have:'
L ' ^\nUnit not a root\nYou have:'
Not dimensionless
W ' sin(m)\n ^\nUnit not dimensionless\nYou have:'
L ' ^\nUnit not dimensionless\nYou have:'
Support search? So if you type "search musical" it returns "musicalcent <nonlinear>, musicalfifth 3|2, musicalfourth 4|3" or an abbreviated version?
Or just build this into pycalcy? Have it query "search" with input text, and generate catalog items for each matching unit?
# These definitions are here for testing of the error checking facilities
# of the units program. All of them are somehow bogus.
# ev100n(x) 2^x / (m2/cd); log2(ev100 m^2/cd)
# bogusunit 1
# bogusunit(x) x+1
# foo meter**
# baz bleganarf
# wronginv(x) 2 x ; 2 x
# boo(x) x+1
# bug(x) boo(x)+x
# bbb(x) boo(12)
# test(x) x^2 ; \
# sqrt(x)
# recur(x) [1] 1+recur(x)
# fezle[kg] 3 4 4 5 5 4 6 3
# foobiz(x) x x ) ; 3
# testa- (3/4)
# testb- (3/4)/(3/4)
# testc- m/kg/hr
# testt(x) [kg;m] x^2-3 ; sqrt(testt+3)
#
#
import launchy
import os
import subprocess
# On Unix, variable should just be empty string ''?
units_path = 'C:/Program Files/GnuWin32/bin/'
# For launching a command windowless in Windows
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
class PyCalcy(launchy.Plugin):
def __init__(self):
launchy.Plugin.__init__(self)
self.name = 'PyCalcy'
self.hash = launchy.hash(self.name)
self.icon = os.path.join(launchy.getIconsPath(), 'calcy.ico')
self.labelHash = launchy.hash('pycalcy')
def init(self):
# Start a Units process here and then send it queries instead of starting a new process for every key press?
pass
def getID(self):
return self.hash
def getName(self):
return self.name
def getIcon(self):
return self.icon
def getLabels(self, inputDataList):
# Ignore unless it has "c |>" at the beginning
if len(inputDataList) < 2:
return
lowerText = inputDataList[0].getText().lower()
if lowerText == 'c':
inputDataList[0].setLabel( self.labelHash )
def getResults(self, inputDataList, resultsList):
# Should only function if there are 2 or 3 arguments in list
if len(inputDataList) != 2:
return
# Should only function if started with "c |>"
if not inputDataList[0].hasLabel(self.labelHash):
return
# Add a short delay to avoid calling units for every single keypress?
# Take the text from the first input item and add a new
# Catalog item with our plugin id
text = inputDataList[1].getText()
# Command for calling units
# can use a list instead to Popen?
# Append desired units to command if it exists
if '>' in text:
expression, desired_units = text.split('>')
command = units_path + 'units -v "' + expression + '" "' + desired_units + '"'
else:
expression = text
command = units_path + 'units -v "' + expression + '"'
# Call Units and get result
proc = subprocess.Popen(command, startupinfo=startupinfo, stdout=subprocess.PIPE)#.wait()
out = proc.communicate()[0]
# Get just the part we want
# (The -1 option isn't available in earlier versions of Units)
result = out.split('\n')[0].strip()
resultsList.push_back( launchy.CatItem(expression,
result,
self.getID(), self.getIcon()) )
def getCatalog(self, resultsList):
pass
def launchItem(self, inputDataList, catItemOrig):
# The user chose our catalog item, print it
catItem = inputDataList[-1].getTopResult()
print 'I was asked to launch: ', catItem.fullPath
launchy.registerPlugin(PyCalcy)
"""Not finished yet"""
import os
import subprocess
# This works on my machines, at least:
PIPE = subprocess.PIPE
startupinfo = None
if os.name == 'nt':
units_path = 'C:/Program Files/GnuWin32/bin/units'
# For launching a subprocess windowless in Windows
# http://code.activestate.com/recipes/409002/
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
if os.name == 'posix':
units_path = '/usr/bin/units'
command = [units_path, '-v']
proc = subprocess.Popen(command, stdin=PIPE, stdout=PIPE, universal_newlines=True, startupinfo=startupinfo)
# Eat up everything to the first prompt
preamble = ''
while 'You have:' not in preamble:
preamble = preamble + proc.stdout.read(1)
def units(expression, desired_units=''):
"""Call GNU Units and return its response
The expression is what you want evaluated, like '50 million grams', and
desired_units is what you want converted to, like 'lb'.
"""
def sanitize(r):
"""Remove all the extraneous stuff"""
r = r[:-10] # Remove '\nYou have:' prompt at end
if 'nt' in os.name: # HACK HACK HACK
r = r.split('\n', 1)[1] # Remove echo of unit at beginning
r = r.strip(' ^\n\t') # Remove ' ^\n' that appears in errors, or Tab that appears with results
r = r.split('\n')[0] # Remove explanatory text after the newline, return just the error itself
# Some tweaks
if r == '':
r = 'Enter an expression'
elif r == 'conformability error' or r == 'reciprocal conversion':
r = r.capitalize()
return r
# Send expression to "You have:" prompt
proc.stdin.write(expression + '\n')
proc.stdin.flush()
# Get response 1 character at a time to avoid blocking
response = ''
while 'You have:' not in response and 'You want:' not in response:
response = response + proc.stdout.read(1)
# Errors or definitions pop back to "You have:" prompt
if 'You have:' in response:
return sanitize(response)
# Send desired units to "You want:" prompt
proc.stdin.write(desired_units + '\n')
proc.stdin.flush()
response = ''
while 'You have:' not in response and 'You want:' not in response:
response = response + proc.stdout.read(1)
# Errors can pop back to "You want:" prompt
if 'You want:' in response:
# Enter nothing and then eat everything back to "You have:" prompt
proc.stdin.write('\n')
proc.stdin.flush()
while 'You have:' not in response:
response = response + proc.stdout.read(1)
return sanitize(response)
def units_command(expression, desired_units=''):
"""Call the units command and get the response (about 50 times slower)"""
comm = command[:]
comm.append(expression)
if desired_units:
comm.append(desired_units)
proc = subprocess.Popen(comm, stdout=PIPE, universal_newlines=True, startupinfo=startupinfo)#.wait()
out = proc.communicate()[0]
result = out.split('\n')[0].strip()
return result
def test():
# Do some tests to see if it handles all known outputs correctly
print('Preamble: ' + preamble[:-10])
print('Conversion\n\t' + units('m', 'ft'))
print('Definition\n\t' + units('km'))
print('Nothing\n\t' + units(''))
print('Conformability (m to W)\n\t' + units('m', 'W'))
print('Unknown unit (q) at You have\n\t' + units('q'))
print('Unknown unit (q) at You want\n\t' + units('m', 'q'))
print('Wrong dimension to function pH(W)\n\t' + units('pH(W)'))
print('Parse error\n\t' + units('('))
print('Reciprocal conversion\n\t' + units('s', 'Hz'))
print('Conformability sum\n\t' + units('1 + m'))
print('Not a root\n\t' + units('sqrt(m)'))
print('Not dimensionless\n\t' + units('sin(m)'))
if __name__ == '__main__':
test()
@endolith
Copy link
Author

(I haven't used this in a long time. I stopped using Launchy when I started using Windows 7, and now Wolfram Alpha also exists for unit calculations (though it's even slower than Google Calc).)

@endolith
Copy link
Author

distance screenshot
thermal noise screenshot

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment