Match statement with strings wildcards

Godot Version

v4.6.stable.official [89cea1439]

Question

My problem is so simple, yet I’m surprised I haven’t even found the question online.

It seems the match statement for literals is… literal. Unlike the String match() method which accepts wildcards :

extends Node

func _ready() -> void:
	var a : String = "arm"
	match a:
		"arm":
			print("arm")
	match a:
		"*arm":
			print("*arm")
	match a:
		"*rm":
			print("*rm")
	match a:
		"*r*":
			print("*r*")
	print("\n")
	if a.match("arm"):print("arm")
	if a.match("*arm"):print("*arm")
	if a.match("*rm"):print("*rm")
	if a.match("*r*"):print("*r*")

prints

arm


arm
*arm
*rm
*r*

Has something so simple been the subject of an issue or pull request already ? Couldn’t find any.
Current options : RegEX or if a.match() elif a.match()

Right, but what is your issue here? The match statement is supposed to be literal whereas the String match() function does something completely different, as it accepts a glob pattern (like a poor man’s regex).

2 Likes

match uses operator == of the operands. Why would you expect it to behave like regex?

1 Like

I’m not saying it should not work like it’s supposed to work.

I was asking if the pattern-matching in the match statement for Strings had been discussed, planned and abandoned, or if there was a pattern or way of doing it that I didn’t think of.

…and I actually found one when looking into how Python would do it. Would you believe it ? it seems to work in GDScript :

extends Node

func _ready() -> void:
	var a : String = "arm"
	match a:
		"arm":
			print("arm")
	match a:
		_ when "rm" in a:
			print("*rm*")
	match a:
		_ when "r" in a:
			print("*r*")

prints

arm
*rm*
*r*

Now, this solution might not be sturdy in the long term, I just tried it, and if you spot a problem down the road in me using it, I’m ready to go and think again. But implying my question was absurd was a lil bit dry.

Repeating match a: kinda defeats the purpose of using match in the first place. Only a single match should be sufficient:

match a:
	"arm":
		print("arm")
	_ when "rm" in a:
		print("*rm*")
	_ when "r" in a:
		print("*r*")

Otherwise you might as well use ifs.

1 Like

I’m still a little confused about this. There are many, many cases where you HAVE to check if a string matches EXACTLY another string. Having wildcards forced onto this would be a nightmare for programmers, and would make many solutions unnecessarily difficult to write, if it was impossible to check a string against another string exactly, with no wildcards.

As others have already discussed, this is already possible using match() because it was MADE for this purpose, whereas comparing two variables at a lower level should never, ever be modified to include wildcards like that. Becase at that point, you are no longer comparing one value to another, you are instead looking for patterns and two entirely different strings can return as being the exact same string. Which is… kind of a huge issue.

1 Like

Repeating match a: kinda defeats the purpose of using match in the first place. Only a single match should be sufficient:

match a:
	"arm":
		print("arm")
	_ when "rm" in a:
		print("*rm*")
	_ when "r" in a:
		print("*r*")

Otherwise you might as well use ifs.

These were examples. Of course I wasn’t going to use them as is.

Witch brings me to a limit I just noticed :

This is my use case :

## name is one of ["head","torso","leg_right","leg_left","arm_right","arm_left","hand_right","hand_lef"]

match name:
	"head":
		pass
	"torso":
		pass
	_ when "arm" in name:
		pass
	_ when "leg" in name:
		pass
	_ when "hand" in name:
		pass

And it won’t work anymore than your example, because the match can only match once, whatever the patterns or pattern guards below it.

match a:
	"arm":
		print("arm")
	_ when "rm" in a:
		print("*rm*")
	_ when "r" in a:
		print("*r*")

prints

arm

and

match a:
	_ when "rm" in a:
		print("*rm*")
	_ when "r" in a:
		print("*r*")

prints

*rm*

So ifs it is…

It was your example, I just re-wrote it in a proper way.

Yeah, match by definition matches only once. And the priority depends on the position of the case. I thought that’s why you opted to using it in the first place.

2 Likes

I’m still a little confused about this. There are many, many cases where you HAVE to check if a string matches EXACTLY another string. Having wildcards forced onto this would be a nightmare for programmers

You can match exactly and match fuzzy with the same function, it’s not revolutionary. Adding a fuzzy behaviour doesn’t make it

impossible to check a string against another string exactly, with no wildcards

var b : String = "armor"
print(b.match("arm")) # match exactly : false
print(b.match("*arm*")) # match fuzzy : true

As is actually already the case with the match statement :

match point:
	[_, 0]:
		print("Point on X-axis")
	[0, _]:
		print("Point on Y-axis")

whereas comparing two variables at a lower level should never, ever be modified to include wildcards like that

You can match fuzzy two Arrays variables because the match statement goes inside the Array and compares the variables element by element, with wildcards possible instead of elements :

Every single element of the array pattern is a pattern itself, so you can nest them.

source

What I learned today is that match won’t go inside a String natively.

Here is another case of

instead looking for patterns :

match x:
	[42, ..]:
		print("Open ended array")

All I was asking was whether there was a String wildcard in the match statement as there are already for Arrays, and as there is in the match() method. There isn’t, point taken.

A second thing I learned today is that sometimes, I better keep my questions to myself. Justifying a simple question in a help forum shouldn’t be necessary.

It actually is necessary in many cases to avoid xy problem situations.

That said, what’s your actual, real-life use case here? Maybe there’s a better approach than doing string wildcards in match statements.

4 Likes

I had faced a problem, for which I provided my solutions in the question itself, so I wasn’t expecting an answer to solve it. I asked if there were wildcards in match statements. It was a simple question.

The code from which the question arose :

# name in ["head","torso","leg_right","leg_left","arm_right","arm_left","hand_right","hand_left"]

func get_crit(damage_type:R_DamageType) -> void:
	
	if name.match("head"):
		match damage_type.name:
			"Slashing":
				chopped_off()
		mob.health.death()
	elif name.match("torso"):
		mob.health.death()
	elif name.match("*leg*"):
		mob.collapse()
		match damage_type.name:
			"Slashing":
				chopped_off()
	elif name.match("*arm*"):
		match damage_type.name:
			"Slashing":
				chopped_off()
	elif name.match("*hand*"):
		match damage_type.name:
			"Slashing":
				chopped_off()

Use a dictionary of callables instead.

As a general advice, whenever you see a bunch of cascading/nested ifs, you need to start thinking about how to re-model the problem better using adequate data structures.

3 Likes

Keep in mind that match (and switch) statements were initially introduced to be more efficient with compilers than if-else blocks. Now under the hood, modern languages often convert if-else chains to match/switch statements to optimize the running of code. So using them is purely aesthetic.

From a purely academic perspective, if you want to do matching like you’re talking about, you’d be better off using Regular Expressions (RegEx). They’re very fast and powerful for string comparisons. Or (as I will show you below) using String built-in functions - which is also faster than shoe-horning match.

Now let’s talk about your code. You are using match incorrectly. It’s like watching someone use a screwdriver as a crowbar. Potentially because you didn’t tell us enough about the data structures you’re using. Potentially because you’re not using GDScript naming conventions and so your naming conventions are confusing.

Is R_DamageType an Enum? Because its name indicates it is. But your code indicates it’s potentially a Resource? So I’m gonna go with that.

class_name Damage extends Resource

enum Type {
	SLASHING,
	PIERCING,
	BLUNT,
	FIRE,
}

@export var name: String = "Slashing"
@export var type: Type = Type.SLASHING

# name in ["head","torso","leg_right","leg_left","arm_right","arm_left","hand_right","hand_left"]

func get_crit(damage: Damage) -> void:
	if name == "head":
		match damage.type:
			Damage.Type.SLASHING:
				chopped_off()
		mob.health.death()
	elif name =="torso":
		mob.health.death()
	elif name.contains("leg"):
		mob.collapse()
		match damage.type:
			Damage.Type.SLASHING:
				chopped_off()
	elif name.contains("arm"):
		match damage.type:
			Damage.Type.SLASHING:
				chopped_off()
	elif name.contains("hand"):
		match damage.type:
			Damage.Type.SLASHING:
				chopped_off()

If you are using a switch statement, there’s no need to wildcard things. Be specific. An Enum is a fast way to have a certain set of options. Leave the name for text output, etc. (Though you can actually convert the enum keys to text if you need to.)

Naming is also important in helping your code be clear. If you REALLY need to know that Damage is a Resource, call it DamageResource. But your code becomes much more readable if you only have one object called Damage.

As for pattern matching, the String class contains a function contains() that returns true if the string you are looking for is contained in the String you use it on. This is faster than using match, and it’s much more readable in code.

Hope that helps.

A Final Note

There’s nothing wrong with asking questions. You started by saying you wonder why no one ever even thought about your solution. The answer people were trying to tell you was that your proposed solution created problems - it didn’t solve them - and they tried to explain why. You appear to have been upset by that.

No one was trying to hurt your feelings. They were trying to explain why there are other solutions. However, as @normalized pointed out, you started by posting a solution without the attendant problem. Most of the people responding to you have years - if not decades - of professional programming experience. We are just trying to help.

2 Likes

Indeed. I am not a big fan of an indent size of 8 but its one big virtue is that you actually start feeling the pain if you got too many levels of indentation. Well, at least with a max line width of 80 characters.

1 Like

Man, this thread went on way farther that it needed to.

But, really, I was not “propos[ing]” my “solution” (in what world could my question be read as me having a solution ?). I was simply asking if the match statement accepted wildcards for Strings. I’ve had my answer.

Didn’t ask for a solution because if name.match(“*a*”) does the job – I will come around and improve on it later, the code is not even complete (isn’t it obvious ? there’s only one damage type yet !) *

didn’t ask about R_DamageType (it’s not a damage, it’s not an enum. it’s not even the topic, I’m not working on that part now),

didn’t state anywhere that I wanted to change a core function to strip it of literal matching (what a bizarre claim !),

didn’t ask for trouble.

I have been feeling answers were wildly attacking straw men, things I didn’t even suggest or mention. I believe it’s caused by people that have years - if not decades - of professional programming experience reading into my question to unearth the One Problem That Is Actually Gnawing At The Newbie - of which there wasn’t any. It’s mostly okay, but giving help that hits beside the mark really comes across as misguided, or as confusing “neophyte” for “neophyte in Godot”. So there’s that, too.

Thank you for .contains("a"). Better than .match(“*a*”) :+1:

And thank you for having - once again - taken the time and tried and be helpful.

*@normalized I read your proposal and, if I understood correctly, I hit a speed bump because I would need a nested collection Dictionary[String,Array[Callable]] to be able to play death() for “head” and “torso” but not for “hand”, to play chopped_off() for “head” and “hand” but not “torso”, etc, something akin to this :

class_name Bodypart

var name : String

var crit_response : Dictionary[String,Array[Callable]] = {
	"head" : [death,damage_type_anim],
	"torso" : [death],
	"leg*" : [damage_type_anim],
	"arm*" : [damage_type_anim,drop_weapon],
	"hand*" : [damage_type_anim,drop_weapon]
	}

func get_crit(damage_type:R_DamageType) -> void:
	for callable in crit_response[name] :
		callable.call(damage_type) #<- would need to process the argument for one Callable and not the others, though

func death() -> void:

(...)

A Dictionary is an interesting route though, and I’ll give it some more thought.

I was just vaguely hinting. For a specific suggestion I’d need to know all the details and requirements of your system. So I’ll leave it to you to figure out how to best structure your data.

The gist is, though, that you can simplify your code a lot by structuring your data cleverly. Let me here plug my favorite system design adage (by Linus Torvalds no less):

Bad programmers worry about the code. Good programmers worry about data structures and their relationships.

Which is just a modernized version of Freed Brooks’ archaic saying (“flowcharts” refers to code and “tables” to data structures):

Show me your flowcharts and conceal your tables, and I shall continue to be mystified. Show me your tables, and I won’t usually need your flowcharts; they’ll be obvious

2 Likes

Apologies for misunderstanding.

1 Like