VisualWorks - Find characters in string and replace them - smalltalk

I have one word. At first, instead of letters, there are only ?. So for example, word 'town' would be shown like this '????'.
Then user guesses letter and if he nails it, it changes from ? to actual letter.
For example, if he guesses t, it would look like this 't???'.
The problem is, that I have no idea, how to go through string and divide it to characters. And if I somehow do it, I cannot change it in the new string.
Should look somehow like this.
word do: [ :c |
c = guessedChar
ifTrue[mask := mask, guessedChar]
ifFalse[mask := mask, '?']
].
mask is initialized to nil, because the word length can change and word is String.
guessedChar is connected to inputField, however it contains only one character at a time.
And would it be better, do it once for every guessedChar or hold collection of all guessed characters and run it over every time?

A String is a Collection of Character objects. So you can use the same methods that apply to other collections, too (like #select:, #collect: or #reject:)
guessedCharacters := 'ts'.
mask := word collect:[:each | (guessedCharacters includes: each)
ifTrue:[each]
ifFalse:[$?]].
Please note that 't' is a String with the Character t. A Character can be written with $ prefix as literal character $t.
As String is a subclass of SequenceableCollection you can concatenate two Strings via ,. You cannot however concatenate a String and a Character.
Instead you could use #copyWith: to append a Character to a String. The result is a new String, it wouldn't modify the existing instance.

You could use
word doWithIndex: [:c :i | c = guess ifTrue: [mask at: i put: c]]
which is equivalent to:
i := 1.
word do: [:c |
c = guess ifTrue: [mask at: i put: c].
i := i + 1]
except that you don't have to initialize and increment i (which is a little bit more error prone, and more verbose)
Addendum
Given that instances of String cannot grow or change their size, which is immutable, I assume that what might change is the variable word. In that case you should initialize mask accordingly, so both strings keep always the same length. Like this:
word: aString
word := aString.
mask := word copy atAllPut: $?
If you also want to preserve the characters already guessed:
word: aString
| guessed |
word := aString.
guessed := mask reject: [:c | c = $?].
mask := word copy atAllPut: $?.
guessed do: [:c | self try: c].
Where #try: is the method we had before
try: aCharacter
word doWithIndex: [:c :i | c = aCharacter ifTrue: [mask at: i put: c]]
(you may want to uppercase things if required)

"initially"
actualWord := 'phlebotomy'.
actualMask := actualWord collect: [:ch| $?].
"after each guess"
word := actualWord readStream.
mask := actualMask readStream.
newMask := WriteStream on: String new.
[ word atEnd
] whileFalse:
[ nextCh := word next = guessedCharcter
ifTrue: [mask skip. guessedCharacter]
ifFalse: [mask next].
newMask nextPut: nextCh
].
actualMask := newMask contents

Related

Smalltalk anomaly - why are the variables always the same, but when computing booleans they are different?

I chose to try out Smalltalk for AOC 2022 puzzle 4. I'm predicating on each line and increment the counter if the constraints are met. I'm trying to understand why the '2-8,3-7' line doesn't met the requirements. Therefore, I started printing out the values to check what's happening. Apparently, when printing out the values by sending displayNl message to the objects, the values firstMax, firstMin etc. are always the same through the loop, containing the info from '2-4,6-8', i.e. the first line. But still, what's even more weird, that the counter gets incremented once, even though the first line doesn't meet the constraints. Then, I figured out that it actually computes the boolean overlapFirst and overlapSecond values correctly, when checking the '6-6,4-6' line, hence ifTrue increments the counter! WHY!?
EDIT: I solved it by putting this instead of first putting the substrings into a variable:
firstAssignment := (line substrings: ',') first.
secondAssignment := (line substrings: ',') last.
Does it mean that you cannot reassign OrderedCollection?
I'm running this with gnu-small talk, by running command:
gst main.st
Here's data.txt.
2-4,6-8
2-3,4-5
5-7,7-9
2-8,3-7
6-6,4-6
2-6,4-8
Here's main.st.
file := FileStream open: 'data.txt' mode: FileStream read.
count := 0.
file linesDo: [
:line |
assignments := line substrings: ','.
firstAssignment := assignments first.
secondAssignment := assignments last.
first := firstAssignment substrings: '-'.
second := secondAssignment substrings: '-'.
firstMin := first first.
firstMax := first last.
secondMin := second first.
secondMax := second last.
overlapFirst := (firstMin <= secondMin) & (firstMax >= secondMax).
overlapSecond := (secondMin <= firstMin) & (secondMax >= firstMax).
overlap := overlapSecond | overlapFirst.
line displayNl.
overlapFirst displayNl.
overlapSecond displayNl.
firstMin displayNl.
firstMax displayNl.
secondMin displayNl.
secondMax displayNl.
overlap ifTrue: [
'Incremented!' displayNl.
count := count + 1.
].
].
Transcript show: count asString.
file close.
This solved my issue... I also edited the post, I'll need to learn how to do things in stackoverflow.
I changed lines 5 and 6.
file := FileStream open: 'data.txt' mode: FileStream read.
count := 0.
file linesDo: [
:line |
firstAssignment := (line substrings: ',') first.
secondAssignment := (line substrings: ',') last.
first := firstAssignment substrings: '-'.
second := secondAssignment substrings: '-'.
firstMin := first first asInteger.
firstMax := first last asInteger.
secondMin := second first asInteger.
secondMax := second last asInteger.
overlapFirst := (firstMin <= secondMin) & (firstMax >= secondMax).
overlapSecond := (secondMin <= firstMin) & (secondMax >= firstMax).
overlap := overlapSecond | overlapFirst.
line displayNl.
overlap ifTrue: [
'Incremented!' displayNl.
count := count + 1.
].
].
Transcript show: count asString.
file close.

Integer to custom type conversion in Ada

I have an array of some kind of objects, indexed by a type index:
type index is new Integer range 1..50;
type table is new Array(index) of expression;
Now, I need to access one of these expressions, depending on a user entry by keyboard. For that I do following:
c: Character;
get(c);
s: String := " ";
s(1) := c;
Finally I can cast the character to type Integer:
i: Integer;
i := Integer'Value(s);
Now, I have the position of the value the user want to access, but Ada doesn't let you access to table, because it is indexed by index and not Integer, which are different types.
What would be the best solution, to access an expression based on the user's input?
type index is new Integer range 1..50;
type table is new Array(index) of expression;
You don't need (and can't have) the new keyword in the declaration of table.
c: Character;
get(c);
s: String := " ";
s(1) := c;
The last two lines can be written as:
S: String := (1 => C);
(assuming that C is visible and initialized at the point where S is declared).
i: Integer;
i := Integer'Value(s);
This is not a "cast". Ada doesn't have casts. It's not even a type conversion. But I understand what you mean; if C = '4', then S = "4", and Integer'Value(S) = 4. (You should think about what to do if the value of C is not a decimal digit; that will cause Integer'Value(S) to raise Constraint_Error.)
Now, I have the position of the value the user want to access, but Ada
doesn't let you access to table, because it is indexed by index
and not Integer, which are different types.
Simple: Don't use different types:
I: Index := Index'Value(S);

extract a substring from a string in smalltalk (squeak)

I'm trying to extract a substring from a string which would be the substring in between 2 delimiters
i.e it should be defined as follows:
substring: aString delimiter: aDelimiter
and, for an example, if i'll get this line:
substring: 'dddd#sss#dddd' delimiter: '#'
the function should return 'sss'.
this is what i've been trying, which didn't work:
substring: aString delimiter: aDelimiter
|index temp1 temp2 sz arr str|
arr := aString asArray.
sz := arr size.
index := arr lastIndexOf: aDelimiter.
temp1 := arr first: (sz - index +1).
index := temp1 lastIndexOf: aDelimiter.
sz :=temp1 size.
temp2 := temp1 first: (sz - index).
str := temp2 asString.
^str.
I don't know if it's worth mentioning but it's supposed to be a class method.
Your basic problem is that the argument aDelimiter is a string instead of a character. You want to call it with $# instead of '#'.
Now for some easier ways. Probably the easiest is to use the subStrings: method:
('dddd#sss#dddd' subStrings: '#') at: 2
This has the disadvantage that it extracts the entire string into substrings separated by the # character which may be more than you need.
The next easiest option is to use streams:
'dddd#sss#dddd' readStream upTo: $#; upTo: $#
That code only extracts the part that you need.
You aren't far from working code, as David pointed out. But I'd just like to point out that it's very procedural. A lot of the magic of Smalltalk, and OOP in general, is writing beautiful, easy to understand code that sends intention revealing messages to a community of appropriate objects. This includes leaning on the objects already existing in the image. I can't think of a time when I've had to go this low level for a simple task like this. It would be great to read one of the many awesome OOP references. My favorite is A Mentoring Course on Smallalk
I think David's solution is right on. I personally like second instead of at: 2, but it feels picky and might be personal preference ('dddd#sss#dddd' subStrings: '#') second
While I like both of the answers above, you might also want to consider another that is closer to your initial attempt and is a little bit more efficient than the others in that it only creates the object you are looking for (e.g., no intermediate Stream)
substringOf: aString delimitedBy: aCharacter
| i j |
i := aString indexOf: aCharacter.
j := aString indexOf: aCharacter startingAt: i + 1.
^aString copyFrom: i + 1 to: j - 1
(Note b.t.w. that I'm also suggesting a slightly different selector.)
Another aspect you would like to consider is how the method should react if aCharacter is not in aString, it is only once or it has three or more occurrences. Something in the lines of:
substringOf: aString delimitedBy: aCharacter
| i j |
i := aString indexOf: aCharacter.
i = 0 ifTrue: [^''].
j := aString indexOf: aCharacter startingAt: i + 1.
j = 0 ifTrue: [^''].
^aString copyFrom: i + 1 to: j - 1
But again, if performance is not a concern in your case, then go for the readStream upTo upTo answer, as it is probably the best.

Maple Sequence Length

I'm trying to create a basic program in Maple that runs the Collatz sequence when given a number (n) from the user. For those that don't know, the Collatz sequence is basically "If the number given is odd, do 3n + 1, if it is even, divide by 2 and continue to do so for every answer. Eventually, the answer will reach 1"
I'm trying to grab the number of iterations that the sequence is performed, say if the sequence is run through 10 times, it prints that out. Here is my current code:
Collatz := proc (n::posint)
if type(n, even) then (1/2)*n
else 3*n+1
end if
end proc
CollSeq := proc (n::posint)
local i;
i := n;
while 1 < i do
lprint(i);
i := Collatz(i)
end do
end proc
This so far works, and if the proc CollSeq(50) is entered, it will perform the Collatz sequence on 50 until it reaches 1. The bit I am stuck on is the length of the sequence. I have read around and learned that I might be able to use the nops([]) function of Maple to get the length of the sequence. Here is what I have tried:
CollLen := proc (n::posint)
local c;
c := CollSeq(n);
print(nops([c]))
end proc
I have a feeling this is horribly wrong. Any help would be much appreciated.
Many Thanks
Your function fails to return the actual sequence of values. You need to accumulate it as you go through the loop.
CollSeq := proc (n::posint)
local i, s;
i := n;
s := i;
while 1 < i do
lprint(i);
i := Collatz(i);
s := s, i;
end do;
s;
end proc
The lprint() command just prints its argument to the terminal (showing it on screen), it DOES not save it in a list. And nops() or a better command numelems() counts the number of elements in a list! So putting nops around something that has lprint will not count the number of things. Instead of using lprint in your second function (procedure), define a list, or better than list, an array and in the lprint-line, use a command to append the new number to your growing collection. If you want to see these numbers, just print this collection. Now this time, your third function can have a meaning and it will work as you expected.
Here is the closest fix to your codes.
Collatz := proc( n :: posint )
if type(n, even) then
return( n/2 ):
else
return( 3*n+1 ):
end if:
end proc:
CollSeq := proc ( n :: posint )
local
i :: posint,
c :: 'Array'( posint ):
i := n:
c := Array([]):
while 1 < i do
ArrayTools:-Append( c, i ):
i := Collatz( i ):
end do:
return( c ):
end proc:
CollLen := proc ( n :: posint )
local c :: posint:
c := CollSeq( n ):
return( numelems( c ) ):
end proc:
Here is a screenshot of using them in a Maple worksheet.
Why do I use an array and not a list? Because if you use a list which is immutable, each time you want to add an element to it, in fact it is defining a new list. It is not a memory efficient way, while array is mutable and your edits modifies the array itself. See the help pages on these in Maple.
And looking at your codes, it seems you have the same problem that some of my students in their first programming course usually have, return and print are not the same thing. If you really want a "print" action, that is fine, but you should not expect that the printed value be the output of the function unless you are using a return line inside the function that returns the same value of the print as well. For example you can have print(c): before return(c): in the second function above. So it both prints the sequence on the terminal and returns it to be used by another function or line of code.

Smalltalk syntax error

I'm learning smalltalk right now and am trying to make a very simple program that creates an array of numbers and then finds the largest number. My code looks like this:
| list max |
list := #(1 8 4 5 3).
1 to: list size do: [:i |
max < (list at: i)
ifTrue: [max := (list at: i)].
ifFalse: [max := max].
].
When I run this code, I get "stdin:7: parse error, expected ']'". I'm a bit confused as to what is causing this. It looks to me like all of my square brackets correspond. Help?
Alexandre told you already that is likely that collection provides a max method. You might be interested in a few ways how to do it.
Using collection max (maximum of all elements)
#(1 8 4 5 3) max
Using number max: (which of two numbers is bigger)
#(1 8 4 5 3) inject: 0 into: [:max :elem|
max max: elem ]
Or using just using the internal iterator
#(1 8 4 5 3) inject: 0 into: [:max :elem|
max < elem
ifTrue: [ elem ]
ifFalse:[ max ] ]
Together with your solution to use an external iteration you can see there are a lot of possibilities.
Hope it adds something
The line number seems off but looking at your code it seems the error is probably caused by the period after ifTrue: [max := (list at: i)]. #ifTrue:ifFalse is a single method selector and it doesn't make sense to break it into two statements.
Actually you could remove the ifFalse part of the code entirely. Assigning max to itself has no effect. Also looping on the indices is not needed here. You can work on the values directly with list do: […].
The max variable should also be initialized. Zero seems like a good choice to compare against the positive numbers in your array.
But instead of doing all that look into the Collection class. Your Smalltalk dialect may already offer a max method for this task.
Your immediate problem is that you terminate the ifTrue section with a period. You could normally just remove the period but, since your ifFalse section is effectively a non-operation, it's probably better to remove that bit.
But even when you fix that, you still need to initialise max so that you don't get a < message being sent to the nil object. You can initialise it to the first element, if there are any, then you can change the loop to start at the second.
Of course, initialising to the first element is problematic when the list is empty so you should handle that as well. In the code below, I've set it to a suitable small value then initialised it from the first element of the list only if it's available.
Corrected code is:
| list max |
list := #(1 8 4 5 3).
max := -99999.
(list size) > 0 ifTrue: [max := (list at: 1)].
2 to: list size do: [:i |
max < (list at: i) ifTrue: [max := (list at: i)].
]
(max) displayNl
This outputs 8 as expected and also works fine on the edge cases (list size of zero and one).