Ceil() with precision Topic is solved

Get help with using AutoHotkey (v1.1 and older) and its commands and hotkeys
User avatar
Chunjee
Posts: 1501
Joined: 18 Apr 2014, 19:05
Contact:

Ceil() with precision

17 Nov 2019, 12:03

I'm looking for some help with a precision ceil function.

AHK comes with ceil() but it does not allow for rounding up to a specified decimal place.

what I came up with:

Code: Select all

ceil(param_number, param_precision:=0) {
    ; prepare data
    precisionMultiple := 1
    if (param_precision > 0) {
        precisionMultiple := abs(param_precision * 100)
    }

    ; create the return
    vValue := round(ceil(param_number * precisionMultiple)/precisionMultiple, param_precision)
    return % vValue
}
needs to result in the following:
ceil(4.05) ; => 5 works currently

ceil(6.004, 2) ; => 6.01 works currently

ceil(6040, -2) ; => 6100 does not work
guest3456
Posts: 3469
Joined: 09 Oct 2013, 10:31

Re: Ceil() with precision

17 Nov 2019, 13:03

finally with the help of google, and after many edits, i've got it working for all of these test cases:

Code: Select all

#Include Yunit.ahk      ; https://github.com/Uberi/Yunit
#Include Window.ahk

Yunit.Use(YunitWindow).Test(CeilTests)

ceiling(param_number, param_precision:=0) {
    ;// based off:  https://stackoverflow.com/a/48933199/312601
    offset := 0.5
    if (param_precision != 0)
      offset := offset / (10**param_precision)
    sum := Trim(param_number+offset . "", "0")                     ;// trim trailing 0s
    value := (SubStr(sum, 0) = "5") ? SubStr(sum, 1, -1) : sum        ;// if last char is 5 then remove it
    result := Round(value, param_precision)
    ; msgbox %sum%`n%value%`n%result%
    return result
}

class CeilTests
{
    Test_NoPrecision()
    {
        Yunit.assert(ceiling(4.05) == 5)
        Yunit.assert(ceiling(4.005) == 5)
        Yunit.assert(ceiling(4.1) == 5)
        Yunit.assert(ceiling(41) == 41)
    }
    
    Test_PositivePrecision()
    {
        Yunit.assert(ceiling(6.004, 2) == 6.01)
        Yunit.assert(ceiling(6.004, 1) == 6.1)
    }
    
    Test_NegativePrecision()
    {
        Yunit.assert(ceiling(6040, -2) == 6100)
        Yunit.assert(ceiling(6040, -3) == 7000)
    }

    Test_ExactPrecision()
    {
        Yunit.assert(ceiling(2.22, 2) == 2.22)
    }
}
Last edited by guest3456 on 17 Nov 2019, 13:38, edited 2 times in total.

User avatar
Chunjee
Posts: 1501
Joined: 18 Apr 2014, 19:05
Contact:

Re: Ceil() with precision

17 Nov 2019, 14:51

Well done :bravo:

If you are game for a lower() I haven't figured that out yet either.
User avatar
Chunjee
Posts: 1501
Joined: 18 Apr 2014, 19:05
Contact:

Re: Ceil() with precision

18 Nov 2019, 11:51

This is what I came up with after checking that stackoverflow

Code: Select all

floor(param_number,param_precision:=0) {
    offset := -0.5
    if (param_precision != 0) {
        offset := offset / (10 ** param_precision)
    }
    ; trim trailing 0s
    sum := Trim(param_number + offset . "", "0")
    ; if last char is 5 then remove it
    value := (SubStr(sum, 0) = "5") ? SubStr(sum, 1, -1) : sum
    result := round(value, param_precision)
    return result
}
But it returns 40 when supplied floor(41); the expectation being 41

To solve that I did 'if whole number and param_precision is 0'

Code: Select all

if (param_precision == 0 && parseint(param_number) == param_number) {
    return floor(param_number)
}
guest3456
Posts: 3469
Joined: 09 Oct 2013, 10:31

Re: Ceil() with precision

18 Nov 2019, 12:02

Chunjee wrote:
18 Nov 2019, 11:51
This is what I came up with after checking that stackoverflow

Code: Select all

floor(param_number,param_precision:=0) {
    offset := -0.5
    if (param_precision != 0) {
        offset := offset / (10 ** param_precision)
    }
    ; trim trailing 0s
    sum := Trim(param_number + offset . "", "0")
    ; if last char is 5 then remove it
    value := (SubStr(sum, 0) = "5") ? SubStr(sum, 1, -1) : sum
    result := round(value, param_precision)
    return result
}
But it returns 40 when supplied floor(41); the expectation being 41

To solve that I did 'if whole number and param_precision is 0'

Code: Select all

if (param_precision == 0 && parseint(param_number) == param_number) {
    return floor(param_number)
}
well if you look at the stackoverflow, its for php which has a ROUND_HALF_UP/DOWN flag.. we don't have that, so i just "rounded down" in the ceil func by trimming off the 5

for floor, after we add the offset, it wants to round the trailing 5 up, so i guess you could parse the string backwards and increment the next number preceeding the 5

User avatar
jeeswg
Posts: 6902
Joined: 19 Dec 2016, 01:58
Location: UK

Re: Ceil() with precision

20 Nov 2019, 00:50

I wrote 2 functions:
Floor/Ceil able to handle decimal places (cf. Excel's Trunc function) - AutoHotkey Community
https://autohotkey.com/boards/viewtopic.php?f=5&t=41539

Do notify of any issues. Thanks.
homepage | tutorials | wish list | fun threads | donate
WARNING: copy your posts/messages before hitting Submit as you may lose them due to CAPTCHA
guest3456
Posts: 3469
Joined: 09 Oct 2013, 10:31

Re: Ceil() with precision

20 Nov 2019, 00:57

jeeswg wrote:
20 Nov 2019, 00:50
I wrote 2 functions:
Floor/Ceil able to handle decimal places (cf. Excel's Trunc function) - AutoHotkey Community
https://autohotkey.com/boards/viewtopic.php?f=5&t=41539

Do notify of any issues. Thanks.
did you even try my test suite above?

your func fails for two of the cases.

User avatar
jeeswg
Posts: 6902
Joined: 19 Dec 2016, 01:58
Location: UK

Re: Ceil() with precision

20 Nov 2019, 02:04

I've run your tests.

It failed this one: Yunit.assert(ceiling(6.004, 1) == 6.1) because of a typo in the function, which I've fixed.

For the other test that it 'failed'. It appears that the test failed, not the function.
Because 2.22 when stored as a Double is bigger than 2.22, and so should round up to 2.23.
Or rather: there are 2 tests worth considering, 'intuitive' and 'mathematical':

Code: Select all

MsgBox, % Format("{:0.17f}", 2.22) ;2.22000000000000020
;Yunit.assert(ceiling(2.22, 2) == 2.22) ;incorrect test? 'intuitive': reduce precision, ignore the final '2'
;Yunit.assert(ceiling(2.22, 2) == 2.23) ;correct test? 'mathematical': consider the full precision
[EDIT:] See some example tests, here:
Ceil() with precision - Page 2 - AutoHotkey Community
https://autohotkey.com/boards/viewtopic.php?f=5&t=69935&p=302588#p302588

That's a good selection of tests you had. Any source?

[EDIT:] Some example code:

Code: Select all

;q:: ;test ceil
vNum := 2.22, vDP := 2
MsgBox, % (vNum * (10**vDP)) ;222.000000
MsgBox, % Ceil(vNum * (10**vDP)) ;223
MsgBox, % Ceil(vNum * (10**vDP)) * (10**-vDP) ;2.230000
MsgBox, % Format("{:0." vDP "f}", Ceil(vNum * (10**vDP)) * (10**-vDP)) ;2.23
return
Last edited by jeeswg on 26 Nov 2019, 01:26, edited 2 times in total.
homepage | tutorials | wish list | fun threads | donate
WARNING: copy your posts/messages before hitting Submit as you may lose them due to CAPTCHA
User avatar
Chunjee
Posts: 1501
Joined: 18 Apr 2014, 19:05
Contact:

Re: Ceil() with precision

20 Nov 2019, 06:15

ceil(2.22, 2) is supposed to be 2.22
ceil(2.22, 1) is supposed to be 2.3
ceil(2.22, 0) is supposed to be 3


all of these compute correctly atm
guest3456
Posts: 3469
Joined: 09 Oct 2013, 10:31

Re: Ceil() with precision

20 Nov 2019, 10:07

jeeswg wrote:
20 Nov 2019, 02:04
because of a typo in the function
vNum vs vNm is a typo.
> 0 vs > 1 is not a "typo".
jeeswg wrote:
20 Nov 2019, 02:04
Any source?
source is my brain
jeeswg wrote:
20 Nov 2019, 02:04
For the other test that it 'failed'. It appears that the test failed, not the function.
Because 2.22 when stored as a Double is bigger than 2.22, and so should round up to 2.23:
Yunit.assert(ceiling(2.22, 2) == 2.22) ;incorrect test?
Yunit.assert(ceiling(2.22, 2) == 2.23) ;correct test?
MsgBox, % Format("{:0.17f}", 2.22) ;2.22000000000000020
its a floating point error that is mentioned in the stackoverflow links.

here's another test that yours fails:
Yunit.assert(ceiling(71.4, 2) == 71.40)
yours similarly gives 71.41

thats like saying assert(2.22=2.22) is an incorrect test, and you think it should be 2.22=2.220000000020. clearly illogical. if you think its logical, remind me not to use your code

don't take offense that your code is wrong, just admit it and fix it. you are the one who asked if there were issues, and so i've told you. you have taken this topic backwards. a working solution was already presented, and you've come and given your basic solution that we already tried and discovered was no good.

Helgef
Posts: 4709
Joined: 17 Jul 2016, 01:02
Contact:

Re: Ceil() with precision

20 Nov 2019, 10:32

Imo, this isn't useful as a math function, it's a string function, I prefer guest3456's result. @jeeswg, If you want it to be a math function that is fine, but I don't see why you'd use format. Anyways, it doesn't produce consistent results,

Code: Select all

msgbox % format("{:.17f}", 0.455) ; 0.45500000000000002
msgbox % jee_ceil(0.455,3) ; 0.455, expected 0.456 since 0.45500000000000002 > 0.455
Cheers.
User avatar
jeeswg
Posts: 6902
Joined: 19 Dec 2016, 01:58
Location: UK

Re: Ceil() with precision

20 Nov 2019, 13:14

@guest3456:
What you're effectively doing is:
MsgBox, % Ceil(222.00000000000002) ;223
And insisting that it should be 222.
I.e. your function rounds before ceiling, which can be a good thing, but by what criteria.
Ideally the logic for this should be fully explained (commented).

In short, floating-point numbers are complicated, and I'd want a more rigorous analysis of the situation, than this, that gives no real justification for the general case, and cites only one example:
PHP Rounding Numbers - Stack Overflow
https://stackoverflow.com/questions/2074527/php-rounding-numbers/48933199#48933199

I wrote a barebones function with the core logic.
To improve it, two considerations are:
- Avoiding using Round or Format, and treating everything as strings, such that the function is completely WYSIWYG, with no black boxes, and consistent in any programming language. This immediately leads to writing a complete maths via strings library.
- Rounding before ceiling/flooring. Perhaps discussing with others a default accuracy to chop to, and possibly adding a parameter to specify this.

Any other considerations would be interesting to hear. *That's why I asked for feedback in my original thread.*

That said, I'd recommend as the best solution, re. accuracy and performance, to store numbers as integers and format them as decimals for presentation.

(Btw there's no need for this: 'clearly illogical', 'remind me not to use your code', 'don't take offense', 'your code is wrong, just admit it'.)
@Helgef:
I've addressed improving the function above.

Over time, the benefits of having a complete understanding of how the AHK built-in functions handle floating-point numbers, has become apparent. E.g. it would help explain the 'consistent results' issue you mentioned.

From experience, you/others are unlikely to want to write a report on that, but it would be most welcome.

A good start would be if you or someone else would explain the Round/Format discrepancies:
Rounding error - AutoHotkey Community
https://autohotkey.com/boards/viewtopic.php?f=5&t=69908

Also, do you have the link for that GitHub discussion, it may have been re. exponentiation. Thanks.
[EDIT:] Perhaps it was this:
Changing operator ** to not cast integer operands to float... by HelgeffegleH · Pull Request #119 · Lexikos/AutoHotkey_L · GitHub
https://github.com/Lexikos/AutoHotkey_L/pull/119
Link:
jeeswg's floats and doubles mini-tutorial - AutoHotkey Community
https://autohotkey.com/boards/viewtopic.php?f=74&t=63650
Last edited by jeeswg on 22 Nov 2019, 09:00, edited 1 time in total.
homepage | tutorials | wish list | fun threads | donate
WARNING: copy your posts/messages before hitting Submit as you may lose them due to CAPTCHA
guest3456
Posts: 3469
Joined: 09 Oct 2013, 10:31

Re: Ceil() with precision

20 Nov 2019, 15:22

jeeswg wrote:
20 Nov 2019, 13:14
@guest3456:
What you're effectively doing is:
MsgBox, % Ceil(222.00000000000002) ;223
And insisting that it should be 222.
no, i'm doing ceil(2.22) and saying the result should be 2.22

if i wanted ceil(2.220000002) then i would've wrote that.

i do not care how AHK or whatever underlying programming language stores their floating point values. that's their problem. we already know your solution is bare-bones, we already tried it and found it insufficient for the specification we require. then you come in this topic without reading the solution presented, just to spam your code, and bring the topic backwards. and now you continue to argue, to defend yourself, instead of just admitting, "yes my func is insufficient for some cases, and i do not care to improve it to fix those cases"

apparently this is the specification and goal for your func:

Code: Select all

msgbox, % JEE_Ceil(0.45, 2)    ; equals 0.45
msgbox, % JEE_Ceil(2.22, 2)    ; equals 2.23 
if you're ok with that, then go for it. you asked to be notified of issues; you've been notified. people are free to use what code they want, for the goals and spec they require.

User avatar
jeeswg
Posts: 6902
Joined: 19 Dec 2016, 01:58
Location: UK

Re: Ceil() with precision

20 Nov 2019, 17:43

2 tests on your ceiling function:

Code: Select all

MsgBox, % ceiling(2.222222, 6) ;2.222223
MsgBox, % ceiling(2.2222222, 7) ;2.2222220

The issue is that SubStr is rounding to 6dp (as set by SetFormat):

Code: Select all

;AHK v1:
vNum := 2.22+0
MsgBox, % Format("{:0.17f}", vNum) ;2.22000000000000020
MsgBox, % SubStr(vNum, 1) ;2.220000
MsgBox, % A_FormatFloat ;0.6

;AHK v2:
vNum := 2.22+0
MsgBox(Format("{:0.17f}", vNum)) ;2.22000000000000020
MsgBox(SubStr(vNum, 1)) ;2.2200000000000002
MsgBox(A_FormatFloat) ;(blank) ;note: SetFormat and A_FormatFloat are AHK v1 only
So, the function needs a consistent approach to how it rounds (reduces the precision), to give an 'intuitive' answer, e.g. so that the floating-point number 2.22 rounds to string 2.22, not string 2.23.
Last edited by jeeswg on 26 Nov 2019, 01:53, edited 3 times in total.
homepage | tutorials | wish list | fun threads | donate
WARNING: copy your posts/messages before hitting Submit as you may lose them due to CAPTCHA
guest3456
Posts: 3469
Joined: 09 Oct 2013, 10:31

Re: Ceil() with precision

20 Nov 2019, 18:12

jeeswg wrote:
20 Nov 2019, 17:43
2 tests on your ceiling function:

Code: Select all

MsgBox, % ceiling(2.222222, 6) ;2.222223
MsgBox, % ceiling(2.2222222, 7) ;2.2222220
touché and good catch

so now i will do, what you are unable to do, that is: i admit that my function is no good for certain cases, and shouldn't be used.

Helgef's suggestion to change this to a string function is probably best, and so that method should be used. find the decimal point, search x chars in either direction based on the sign of the precision param, and then increment the previous char. and now the reason for unit testing becomes apparent becuase now we have an even larger testing suite to test our new func against

User avatar
jeeswg
Posts: 6902
Joined: 19 Dec 2016, 01:58
Location: UK

Re: Ceil() with precision

20 Nov 2019, 19:37

I don't think that your function 'shouldn't be used'. It may be that it's fine for precisions of up to 5, and possibly more with some slight tweaks.
So, all that would be needed is a comment stating the valid range. Although such statements can be complicated to prove.

Floating-point numbers are perhaps the biggest headache in IT. They're a can of worms. A can of floating worms.
Even the AHK operators/functions could have comments added:

Code: Select all

MsgBox, % (2.22 = 2.2200000000000002) ;1 (expected 0)
MsgBox, % (0.1 = 0.3-0.2) ;0 (expected 1)

MsgBox, % Mod(12.345, 0.01) ;0.005000 (as expected)
MsgBox, % Mod(12.34, 0.01) ;0.010000 (expected 0)
MsgBox, % Mod(1.23, 0.01) ;0.010000 (expected 0)

I accepted that my function could be made more intuitive. E.g. to ignore the final '2' in '2.2200000000000002'.
However, I need a good source for what a typical cut-off value might be. That's why I haven't rushed to change the function yet.
Anyhow, I'd probably keep my current Ceil/Floor functions unchanged, but with a different name, because their current behaviour can be advantageous. Cf. >> and >>> (logical right shift) which are both useful.
Last edited by jeeswg on 26 Nov 2019, 07:41, edited 1 time in total.
homepage | tutorials | wish list | fun threads | donate
WARNING: copy your posts/messages before hitting Submit as you may lose them due to CAPTCHA
Helgef
Posts: 4709
Joined: 17 Jul 2016, 01:02
Contact:

Re: Ceil() with precision

21 Nov 2019, 02:47

Helgef's suggestion to change this to a string function is probably best
What I meant was that you want a formated string, not a pure float number, i.e, if you input 2.22 and want a precision of 2, you don't think of the input as 2.22000000000000020 even though that is a more accurate representation of the floating point value. But if you do want to interpret it as 2.22000000000000020 , as jeeswg suggested, returning the string "2.23" doesn't makes sense. If you consider the 17th decimal in the input, you should return the 17 decimal in the output, i.e., 2.22 should output the float value 2.23 which by the way is better represented by the string 2.22999999999999998 (on 64 bit, 32 bit differs.).

@guest3456, what dictates the behaviour of your function is setformat, when you convert the float value to string in trim(param_number+offset . "", "0"), I'd guess this version would work better, at least for precision values of 15 and less,
Edit: :arrow: Updated.

Code: Select all

ceiling(param_number, param_precision:=0) {
    ;// based off:  https://stackoverflow.com/a/48933199/312601
    offset := 0.5
    if (param_precision != 0)
      offset := offset / (10**param_precision)
    sum :=  param_precision >= 1 ? Trim(format("{:." param_precision+1 "f}", param_number+offset) . "", "0")                     ;// trim trailing 0s
								: Trim(param_number+offset . "", "0")
	value := (SubStr(sum, 0) = "5") ? SubStr(sum, 1, -1) : sum        ;// if last char is 5 then remove it
    result := Round(value, param_precision)
    ; msgbox %sum%`n%value%`n%result%
    return result
}
You could use setformat but it is deprecated and format is recommended instead.

Also note that your tests should quote float values to force string comparison, eg, Yunit.assert(ceiling(2.22, 2) == "2.22"), you can get false positives, eg,

Code: Select all

msgbox %  ceiling(2.22, 2) == 2.22 ; false positive
msgbox %  ceiling(2.22, 2) == "2.22" ; catches error
ceiling(a,b){
	return "2.22000000000000020"
}
Cheers.
Last edited by Helgef on 24 Nov 2019, 04:02, edited 1 time in total.
User avatar
jeeswg
Posts: 6902
Joined: 19 Dec 2016, 01:58
Location: UK

Re: Ceil() with precision

21 Nov 2019, 10:33

So, rounding, before flooring/ceiling, can give you more 'intuitive' results.
From doing some tests, it seems best to use Round (or Format), to give you a string rounded to 15 decimal places. You can then apply RTrim to remove trailing zeros.
I.e. you want to use the maximum precision to preserve the number as much as possible, but you want to lose a bit of precision to ignore some of the trailing digits.

15 decimal places appeared to give 'intuitive' results for numbers of the form 0.x (0.1 to 0.9) to 0.xxxxxxxxx (0.000000001 to 0.999999999) (9 decimal places).
Perhaps 99% of the time, people want 9 decimal places or fewer.

(A slightly simplified script took just under 80 minutes to test 9 decimal places. Note: for each additional digit, the script takes 10 times longer.)

For Floor, you could just truncate that string.
For Ceil, you'd have to inspect digits from right-to-left, possibly incrementing some digits, possibly adding a 1 to the start of the number.
You could also remove the decimal point, apply Ceil, then restore the decimal point, and store the result as a string.

Some tests:

Code: Select all

;q:: ;test Format on floating-point numbers

;e.g. vLen := 3, numbers from 0.001 to 0.999,
;when rounded to 15 decimal places and trailing zeros trimmed,
;the numbers matched in appearance
;e.g. MsgBox, % "" RTrim("0.563", "0") = RTrim(Round(0.563, 15), "0") ;1
;e.g. MsgBox, % "" RTrim("0.563", "0") = RTrim(Round(0.563, 16), "0") ;0
;e.g. MsgBox, % "" RTrim("0.563", "0") = RTrim(Round(0.563, 17), "0") ;0

;maximum number of decimal places where appearances matched:
;vDP := 16, vLen := 1
;vDP := 15, vLen := 2
;vDP := 15, vLen := 3
;vDP := 15, vLen := 4
;vDP := 15, vLen := 5
;vDP := 15, vLen := 6
;vDP := 15, vLen := 7

;an example where some appearances don't match:
vDP := 16, vLen := 3

;vDoListAll := 1
vDoListAll := 0

vCount := (10**vLen) + 1
vCountDiff := 0
vOutput := ""
VarSetCapacity(vOutput, 1000000*2)
Loop % vCount
{
	vNum := (A_Index-1)/(vCount-1)
	vNum1 := Format("{:0.20f}", vNum)
	vNum2 := Format("{:0." vDP "f}", vNum)
	vNum3 := Round(vNum, vDP)
	vNum2X := RTrim(vNum2, "0")
	vNum3X := RTrim(vNum3, "0")
	if vDoListAll
	|| (StrLen(vNum2X) > vLen+2)
	|| (StrLen(vNum3X) > vLen+2)
		vOutput .= vNum "`t" vNum1 "`t" vNum2 "`t" vNum3 "`r`n"
		, vCountDiff++
		;, MsgBox(vOutput)
}

Clipboard := vOutput
vLimit := 10000
if (StrLen(vOutput) <= vLimit)
	MsgBox, % "difference count: " vCountDiff "`r`n" vOutput
else
	MsgBox, % "difference count: " vCountDiff "`r`n" SubStr(vOutput, 1, vLimit) "..."
return
homepage | tutorials | wish list | fun threads | donate
WARNING: copy your posts/messages before hitting Submit as you may lose them due to CAPTCHA

Return to “Ask for Help (v1)”

Who is online

Users browsing this forum: macromint, peter_ahk, Spawnova, wineguy and 274 guests