String as float evaluates to 0!

With all my respect and love to SC and it’s creators in mind:

this is a catastrophe in terms of language design: "random_string".asFloat returns 0.0!
Same way behave .asInteger etc.

This caused me today a long time looking for the reasons of unexpected behaviors in my code, having no idea why I am having a 0 somewhere shouldn’t be a zero! I just came upon the reason by accident trying every byte of my code and checking the behavior.

This relates to this discussion perhaps Remove Object.as* method to make refactoring easier · supercollider/supercollider · Discussion #6065 · GitHub

To understand your question more (and also some cases I was confronted with before, which I cannot remember exactly now, but I had some similar problems), what do you expect as a result of "hello".asFloat? Some of SCLANG’s behaviour with strings containing numbers is as follows:

"11".asInteger // -> 11
"hello11".asFloat // -> 0.0
"11hello".asFloat // -> 11.0
"hel_11_lo".asFloat // -> 0.0
"1hel_11_lo1".asFloat // -> 1.0

But how can you check if a string is a valid representation of a float unless the conversion method tells you? I would expect a non-valid String to return nil (or throw) when .asFloat is called rather than 0.0, no? (Answering @smoge not @prko sorry!)

Exactly, you can’t.That’s why I’m surprise one would use it in their code without considering it can fail, that’s my point.

The language is hiding what is really happening. When you use String.asFloat, the best you get is a maybe float, and the fallback case returns zero. It is not written anywhere in the code. If you do this in Haskell, where this would be very explicit, you would need a fromMaybe 0

EDIT: ok, you can return nil but then the function won’t be String → Float anymore (it will be effectively a String → Maybe Float in practice) and then you also need to take precaution. From a language design point of view, it would be better, I think.

To get numbers from strings, I could imagine the following three code structures:

1. string is really random:

(
~ifStringIsRandom = { |string| 
	case
	{ "^[0-9]".matchRegexp(string) } { string.asFloat }
	{ "^[0-9][0-9]$".matchRegexp(string) && (string.size > 1) } { string.asFloat }
	{ "^[0-9]".matchRegexp(string).not } { string }
}
)
~ifStringIsRandom.("1") // -> 1.0
~ifStringIsRandom.("11") // -> 11.0
~ifStringIsRandom.("1string") // -> 1.0
~ifStringIsRandom.("string") // -> string
~ifStringIsRandom.("str1.0ing") // -> str1.0ing
~ifStringIsRandom.("string1") // -> string1

2. string is a valid sclang code

(
~ifStringIsSCLANGcode = { |code| code.interpret }
)
~ifStringIsSCLANGcode.("1") // -> 1
~ifStringIsSCLANGcode.("x = { SinOsc.ar * 0.1 }.play") // -> Synth('temp__2' : 1004)
~ifStringIsSCLANGcode.("x.free") // -> Synth('temp__2' : 1004)

3. collecting only digits from a given string, regardless of the string’s content
(@semiquaver Below nil returns if a string contains no numbers.)

(
~toGetNumbersOnly = { |string| 
	var digitIndices = string.findAllRegexp("[0-9]");
	if (digitIndices.size == 0) { nil 
	} {
	string[digitIndices].join
	}
}
)

~toGetNumbersOnly.("1") // -> 1
~toGetNumbersOnly.("11") // -> 11
~toGetNumbersOnly.("1string") // -> 1
~toGetNumbersOnly.("string") // -> nil
~toGetNumbersOnly.("string1") // -> 1

~toGetNumbersOnly.("str1.0ing") // -> 10 
// I cannot find a way to get 1.0. 
// see the comment below.

comment
sclang does not behave correctly with “\.”

"stri1.0ng".findAllRegexp("\.")
"\.".matchRegexp("stri10ng")

Also you could mean the followings:

"string".size

"string".digit
"string".digit.join

"string".ascii
"string".ascii.join

It is unclear what you mean…

In the expression "\.", the backslash is an escape character applied to the dot, so the result is ".".

If you want backslash-dot, then you have to apply an escape-backslash to the content-backslash: "\\." is a string whose contents are \..

hjh

1 Like

@jamshark70 Thank you very much! Now it works properly!!!

(
~toGetNumbersOnly = { |string| 
	var digitIndices = string.findAllRegexp("[0-9]|\\.");
	if (digitIndices.size == 0) { 
		nil 
	} {
		string[digitIndices].join.asFloat
	}
}
)

~toGetNumbersOnly.("1") // -> 1.0
~toGetNumbersOnly.("11") // -> 11.0
~toGetNumbersOnly.("1string") // -> 1,0
~toGetNumbersOnly.("string") // -> nil
~toGetNumbersOnly.("string1") // -> 1.0
~toGetNumbersOnly.("str1.0ing") // -> 1.0

For reference sclang uses Perl style regex. From String help:

https://www.boost.org/doc/libs/1_69_0/libs/regex/doc/html/boost_regex/syntax/perl_syntax.html

2 Likes

It definitely would be a great help if SC would throw an error (errors are nice and save your day!), as soon as a string can’t (for human readers) be converted to a floating point number! I think something like this is way more intuitive:

"11".asFloat => 11.0
"11".asInteger => 11
"11.5".asInteger => 11 (or error, because of other literals than numbers)
"11.5".asFloat => 11.5
"2e3".asFloat => 2000.0
"2e3".asInteger => 2000 (or error, because of other literals than numbers)
"hello".asFloat => Error
"hello".asInteger => Error

sclang is dynamically typed. We usually do not need to worry about the data type, but in some cases it will return a “Wrong type” error:

For example:

"s".matchRegexp('s')

returns

ERROR: Primitive ‘_String_Regexp’ failed.
Wrong type.

I am not sure if the followings should be returned as such “Wrong type” errors:

I think we should currently use one of the three code constructions I suggested above. However, changing the sclang behaviour also sounds right!

Yes, that would be much better. What’s the point of a conversion method if it can’t signify failure? Unfortunately, I’m not sure if we can improve this while maintaining backwards compatibility…

2 Likes

That is true for things like duck typing, but here we are talking about type conversions. Conversions can fail, so we need to either throw an error or return a sentinel value (e.g. nil). Returning 0.0 is just bad.

1 Like

There is also the case of .asInteger. I don’t really know how to reproduce this, but I have encountered situations where value = 7.0 (in the post window) but value.asInteger = 6. For this reason I never rely on float.asInteger and always use float.round.asInteger.

2 Likes

Then, error message is better than nil!

I checked C and Java’s string-to-float functions.

So Java does throw an exception for a non-numeric string, but C++ does not.

I’m not disagreeing with the idea that an error would be better, but it’s worth noting that SC’s behavior does have precedent. It’s definitely arguable that it’s a bad precedent, but SC isn’t just making it up out of thin air. (The SC primitive very likely just wraps ascii-to-double = atod().)

Perhaps a different method name for asFloat with error?

hjh

1 Like

This behavior is generally regarded as a mistake. Even C has a more robust version: https://cplusplus.com/reference/cstdlib/strtod/

In C++ you would rather use std::stof resp. std::stod (std::stof, std::stod, std::stold - cppreference.com) which throws an exception if no conversion can be performed!

(C++17 added std::from_chars (std::from_chars - cppreference.com), which returns an error code instead of throwing an exception. It also uses the C locale by default and is supposed to be the fastest among all conversion functions in the standard library.)

3 Likes