Understanding complicated numbers

I’m nearly to the end of my conversion of Sand-Dancer to Dialog.

Here’s part of the code for a radio that you can tune, from the Inform7 source:

A frequency is a kind of value. 100.9kHz specifies a frequency. 
100.9 kHz specifies a frequency. 
100.9 specifies a frequency with parts integer and decimal.

The radio has a frequency called the frequency tuned to. 
The frequency tuned to of the radio is 77.2kHz. 
The radio has a frequency called the maximum frequency. 
The maximum frequency of the radio is 109.9kHz. 
The radio has a frequency called the minimum frequency. 
The minimum frequency of the radio is 67.0kHz.

Tuning it to is an action applying to one thing and one frequency. 
Understand "tune [thing] to [frequency]" as tuning it to.  
Understand "turn [thing] to [frequency]" as tuning it to.

Understand "tune [thing] to [number]" as coarse tuning.
Coarse tuning is an action applying to one thing and one number.
Carry out coarse tuning: try tuning the noun to the frequency with integer part number understood decimal part 0. 

So … seems like Inform is probably injecting ton of Inform6 code to support this (we’ll know, someday, when it is open source). I’m at a loss as to how to even start. Certainly, using a two element list for the frequence, but the process of converting input to a frequency value and back again is … a lot.

I’m really missing a little regular expression parsing on this one! Any thoughts?

You can look at the I6 code after compiling. It’s not very nice-looking but it’s conceptually simple. Look at the characters one at a time; keep track of whether you’re expecting a digit, “.”, “k”, “h”, or “z” next.

Inform doesn’t use a two-element list, actually. It stores a frequency value of 77.2kHz as the integer 772.

In general, I’d propose something like…

(word $Word means frequency $Freq)
    (split word $Word into $Chars)
    (split $Chars by @\. into $Left and $Right)
    (join $Left into $Leftword)
    (understand $Left as number $First)
    …

However, the . character is treated specially by the Dialog parser, and will always appear in a word on its own. And moreover, the first stage of the parser (after figuring out dictionary words) is to split the input on . and then, so your program would never be given all the words together!

The best I can think of is to modify the definition of (parse commandline $Words) to call a new predicate (early rewrite $Input into $Output) before doing any parsing, then look for periods between numbers and turn them into some other character of your choice ( or the like) before handing it back to the parser.

But this is…not especially elegant. There’s been talk of a preprocessing stage between input and tokenization, which would make this significantly easier, but it doesn’t work with Dialog’s current charset-independent model.

So while this is theoretically possible, unfortunately, I doubt it can be done in a very pleasant way. It may be easier just to move the setting to Europe and use a comma for the decimal separator instead.

Now that I think about it more, maybe a preprocessing stage (as mentioned in the other thread) using single-character dictionary words instead of Unicode codepoints, to maintain charset-independence? On the Z-machine side this isn’t too difficult (@aread text_buffer 0; @call Preprocess; @tokenize text_buffer parse_buffer; in Inform assembly syntax) but this would be very blatantly a feature request.

(or make it an analog TV with no video! Integer channel numbers for everyone, just as Dialog intended! But yes, not the point of the exercise…)

I chose to shortcut the puzzle: you can find the right frequency elsewhere and then you know it when you tune the radio. It’s passable.

Okay! I think I solved this! It’s horribly inefficient and probably not very elegant, but it works!

First of all, here’s the magic:

(command preprocessing enabled)

(preprocessing entry point $In $Out)
	(match pattern [10 [@. @$] 10 [k [@\s k]] h z] in $In giving $Out)

Basically, this pattern is looking for digits, followed by a period, followed by digits, followed by khz. When it finds it, it replaces the period with a $ and inserts some extra space before the khz, just in case.

Then, we add some code to parse frequencies:

(understand $Words as frequency $Freq)
	(split $Words by @$ into $Left and $Right)
	($Right = [$Right2 @khz])
	(understand $Left as number $First)
	(understand [$Right2] as number $Second)
	($Freq = [$First $Second])

@(grammar transformer [[frequency] | $Tail] $SoFar $Verb $Action $Rev)
	(grammar transformer $Tail [90 | $SoFar] $Verb $Action $Rev)

(match grammar token 90 against $Words $ into $Freq)
	*(understand $Words as frequency $Freq)

And we add an action to show it off:

(grammar [tune to [frequency]] for [tune to $])
(action [tune to $] may group $ with $) %% Hack

(group-perform [tune to $Freq])
	($Freq = [$X $Y])
	You align yourself to the mystic frequency of $X . (no space) $Y khz.

(Note that this uses group-perform rules, because the simple parsing here stores the parts of the number as a list, and the parser machinery interprets that as a multiple action. A more robust implementation would find some way to avoid this.)

For this to work, you’ll need preprocess.dg, patterns.dg, and numberhack.dg (though this third one shouldn’t be needed for very long now: it’s just a workaround for a minor Dialog bug).

Hope this helps!

3 Likes

And a followup, getting even closer to the I7 version by means of Kinds-of-Value:

(understand $Words as frequency $Freq)
	(split $Words by @$ into $Left and $Right)
	($Right = [$Right2 @khz])
	(understand $Left as number $First)
	(understand [$Right2] as number $Second)
	(struct $Freq has tag @frequency and contents [$First $Second])
	(fully bound $Freq) %% It will always be fully bound, but the compiler can't be certain about that unless we tell it

(perform [tune to $Freq])
	(struct $Freq has tag @frequency and contents [$X $Y])
	You align yourself to the mystic frequency of $X . (no space) $Y khz.

No more messing around with group-perform rules! Which means you can even tune to multiple frequencies at once now, if you really want to.

2 Likes

>BOTH SENSA AND POET, TUNE TO 105.9kHz AND 94.7kHz

3 Likes

I’m still trying to get tune radio to 103.2 to work, where there isn’t a khz suffix. I don’t think this pattern is right, but I’ve been experimenting and not finding anything that matches correctly.

    (match pattern [10 [@. @$] 10] in $In giving $Out)

I’ve tried adding explicit @\s before and after the decimal point, no dice. Any advice?

Hm. That should work, but the whole extension was really made as a proof-of-concept and hasn’t been tested as much as it should have been.

I’ll take a look!

Here’s the code I currently have, which works for a decimal with khz suffix, or an integer value with a suffix; later in the code we match on number to handle integer value with no suffix.

I haven’t been able to reproduce the problem; this works for me.

(command preprocessing enabled)
(preprocessing entry point $In $Out)
	(match pattern [10 [@. @$] 10] in $In giving $Out)

(understand $Words as frequency $Freq)
	(split $Words by @$ into $Left and $Right)
	(understand $Left as number $First)
	(understand $Right as number $Second)
	(struct $Freq has tag @frequency and contents [$First $Second])
	(fully bound $Freq)

(With the previous support code around it.)

I suspect the problem is with the disjunction in your preprocessing entry point. I didn’t design the pattern-matcher to fail if it can’t match anything; it may do that, but if so it’s by accident.

This means you should match multiple patterns in series, not in parallel:

(preprocessing entry point $In $Out)
    (match pattern […] in $In giving $A)
    (match pattern […] in $A giving $B)
    (match pattern […] in $B giving $Out)

This is also more robust, even if the pattern-matcher were adjusted to fail if there was no match: it means something like >TUNE TO 11.111 KHZ THEN TUNE TO 22.222 will still be understood, which requires both patterns to be matched and replaced.

Got it working nicely in Sand-dancer v5.

2 Likes

Had to add the following to your override of (try-complex $):

	(if) ~(action $ComplexAction preserves the question) (then)
		(now) ~(implicit action is $)
		(now) ~(implicit action wants direction)
	(endif)

Copied from standard library. It explains the “sticky verb” problem I was having in Sand-dancer.

See this commit.

1 Like