Skip to content

Commit

Permalink
[blockly] Support Quantity in more math blocks (#2041)
Browse files Browse the repository at this point in the history
Closes #2001.

Adds Quantity support for more math blocks:
- math_single had to be reimplemented
- math_minmax was added

Note that there is a special case on min/max if the inputs are not of
equal type an error will be shown to user.

In the special case of variables Blockly does its best to detect the
right code to be generated for the min/max block:
- both are variables -> then numerical input is expected
- one of inputs is a variable: then blockly uses the type of the other
non-var-block to base the generation on (either number or quantity
comparison)
- note that no type conversion of the inputs is done

Also-by: Florian Hotze <florianh_dev@icloud.com>
Signed-off-by: Stefan Höhn <mail@stefanhoehn.com>
  • Loading branch information
stefan-hoehn authored Oct 8, 2023
1 parent aaa5e7a commit e2ebc1d
Show file tree
Hide file tree
Showing 2 changed files with 189 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ export default function (f7, isGraalJs) {
const decimals = javascriptGenerator.valueToCode(block, 'DECIMALS', javascriptGenerator.ORDER_NONE)
const operand = block.getFieldValue('op')

let code = ''
let code
if (operand !== 'toFixed') {
let method = ''
switch (operand) {
Expand All @@ -162,4 +162,180 @@ export default function (f7, isGraalJs) {
}
return [code, 0]
}

Blockly.Blocks['math_single'] = {
init: function () {
const dropDown = new Blockly.FieldDropdown([
['square root', 'ROOT'],
['absolute', 'ABS'],
['-', 'NEG'],
['ln', 'LN'],
['log10', 'LOG10'],
['e^', 'EXP'],
['10^', 'POW10']
])
this.appendValueInput('NUM')
.setCheck(['Number', 'oh_quantity'])
.appendField(dropDown, 'OP')

this.setColour('%{BKY_MATH_HUE}')
this.setInputsInline(false)
let thisBlock = this
this.setTooltip(function () {
const operand = thisBlock.getFieldValue('OP')
switch (operand) {
case 'ROOT': return 'Return the square root of the input'
case 'ABS': return 'Return the absolute value of the input'
case 'NEG': return 'Return the negation of the input'
case 'LN': return 'Return the natural (base e) logarithm of the input'
case 'LOG10': return 'Return the base 10 logarithm of the input'
case 'EXP': return 'Return e to the power of the input'
case 'POW10': return 'Return 10 to the power of the input'
}
})
this.setHelpUrl('https://www.openhab.org/docs/configuration/blockly/rules-blockly-math.html#functions')
this.setOutput(true, 'Number')
}
}

javascriptGenerator['math_single'] = function (block) {
const inputType = blockGetCheckedInputType(block, 'NUM')
const math_number_input = javascriptGenerator.valueToCode(block, 'NUM', javascriptGenerator.ORDER_FUNCTION_CALL)
let math_number = math_number_input
if (inputType === 'oh_quantity') {
math_number = math_number_input + '.float'
}
const operand = block.getFieldValue('OP')

let method = ''
switch (operand) {
case 'ROOT':
method = `Math.sqrt(${math_number})`
break
case 'ABS':
method = `Math.abs(${math_number})`
break
case 'NEG':
method = `-${math_number}`
break
case 'LN':
method = `Math.log(${math_number})`
break
case 'LOG10':
method = `Math.log10(${math_number})`
break
case 'EXP':
method = `Math.exp(${math_number})`
break
case 'POW10':
method = `Math.pow(10, ${math_number})`
break
}

let code = `${method}`

if (inputType === 'oh_quantity') {
code = `Quantity((${code}).toString() + ' ' + ${math_number_input}.symbol)`
}
return [code, javascriptGenerator.ORDER_FUNCTION_CALL]
}

Blockly.Blocks['oh_math_minmax'] = {
init: function () {
const dropDown = new Blockly.FieldDropdown([
['minimum of', 'min'],
['maximum of', 'max']
])
this.appendDummyInput()
.appendField(dropDown, 'OP')
this.appendValueInput('NUM1')
.setCheck(['Number', 'oh_quantity'])
this.appendValueInput('NUM2')
.appendField(' and ')
.setCheck(['Number', 'oh_quantity'])

this.setColour('%{BKY_MATH_HUE}')
this.setInputsInline(true)
let thisBlock = this
this.setTooltip(function () {
const operand = thisBlock.getFieldValue('OP')
switch (operand) {
case 'min': return 'Return the mimimum of both inputs'
case 'max': return 'Return the maximum of both inputs'
}
})
this.setOnChange(function (changeEvent) {
if (changeEvent.type === 'move') {
const typeInfo = computeMinMaxOutputType(this)
this.setOutput(true, typeInfo.leadType)
}
})
this.setHelpUrl('https://www.openhab.org/docs/configuration/blockly/rules-blockly-math.html#minmax')
this.setOutput(true, null)
}
}

function computeMinMaxOutputType (block, throwError = false) {
const operand = block.getFieldValue('OP')

let math_number_input1
let math_number_input2
try { // may throw an exception on workspace startup in uninitialized state but in this case we can ignore
math_number_input1 = javascriptGenerator.valueToCode(block, 'NUM1', javascriptGenerator.ORDER_FUNCTION_CALL)
math_number_input2 = javascriptGenerator.valueToCode(block, 'NUM2', javascriptGenerator.ORDER_FUNCTION_CALL)
} catch (e) {}

/*
When dealing with variables, Blockly does not provide type information (type is "").
In this case, we fall back to checking whether the actual input contains "Quantity" or is a number.
*/
let inputType1
let inputType2
try { // may throw an exception on workspace startup in uninitialized state but in this case we can ignore
inputType1 = blockGetCheckedInputType(block, 'NUM1') || getVariableType(math_number_input1)
inputType2 = blockGetCheckedInputType(block, 'NUM2') || getVariableType(math_number_input2)
} catch (e) {}

/*
If exactly one of the two inputs is a variable, assume it has the same type as the other input.
In case both inputs are vars, assume they are numbers.
*/
const containsOneVar = (inputType1 === '' && inputType2 !== '') || (inputType1 !== '' && inputType2 === '')

/*
If both inputs are not the same type and none of them is a variable, throw an Error on code generation.
*/
if (throwError && inputType1 !== inputType2 && !containsOneVar) {
throw new Error(`Both operand types need to be equal for ${operand.toUpperCase()}-block (${math_number_input1} -> ${inputType1}, ${math_number_input2} -> ${inputType2})`)
}

const leadType = inputType1 || inputType2 || 'Number'
return { leadType, math_number_input1, math_number_input2, operand }
}

javascriptGenerator['oh_math_minmax'] = function (block) {
const { leadType, math_number_input1, math_number_input2, operand } = computeMinMaxOutputType(block, true)
let code = ''

switch (leadType) {
case 'oh_quantity':
const op = (operand === 'min') ? 'lessThan' : 'greaterThan'
code = `(${math_number_input1}.${op}(${math_number_input2})) ? ${math_number_input1} : ${math_number_input2}`
break
default:
code = `Math.${operand}(${math_number_input1},${math_number_input2})`
break
}

return [code, javascriptGenerator.ORDER_FUNCTION_CALL]
}

/*
* As Blockly does not provide type information for variables, try to determine it based on the content of the block.
*/
function getVariableType (math_number_input) {
if (math_number_input.includes('Quantity')) return 'oh_quantity'
if (!isNaN(math_number_input)) return 'Number'
return ''
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,18 @@
</shadow>
</value>
</block>
<block type="oh_math_minmax">
<value name="NUM1">
<shadow type="math_number">
<field name="NUM">3</field>
</shadow>
</value>
<value name="NUM2">
<shadow type="math_number">
<field name="NUM">4</field>
</shadow>
</value>
</block>
<block type="math_on_list" />
<block type="math_modulo">
<value name="DIVIDEND">
Expand Down

0 comments on commit e2ebc1d

Please sign in to comment.