検索

Article
· Dec 31, 2024 6m read

My little Advent of Code 2024 journey

You'll find the original text and all my Advent of Code efforts in different programming languages in https://bitbucket.org/janihur/advent-of-code/src/master/

ObjectScript code discussed here is found in https://bitbucket.org/janihur/advent-of-code/src/master/objectscript

I have been using ObjectScript only on last 1,5 years maybe 30% of my working time but the code at work is very different than in these puzzles. You can find my random code snippets and learning notes from https://github.com/janihur/objectscript-ex

This is the first time using ObjectScript during the contest. Earlier this year I practiced a bit with first 5 puzzles from 2018.

Before the event I made version 2 of my input data load routine. The main motivation is to be able to make the data available directly from the VS Code editor without making data files available for IRIS instance.

The solutions expect a data from object that implements aoc.data.base "iterator" interface. The class can be created from the input data:

python3 data-import-v2.py PUZZLE_INPUT_FILE

This will generate data classes:

  • aoc.example<YEAR><DAY>.cls - will be create only if PUZZLE_INPUT_FILE-example exists
  • aoc.input<YEAR><DAY>.cls

The solution classes have the interface with parameter defining if it should load the example or the actual puzzle data:

ClassMethod Solve(useExampleData As %Boolean = 0)

Some solutions uses companion classes, so be sure to load them too:

  • aoc.Coord
  • aoc.list
  • aoc.stack
  • aoc.StopWatch
  • aoc.str
  • aoc.topo
  • aoc24.Guard

Day 1

The data loading pattern works just fine. The trick is to use regular expressions (especially %Regex.Matcher class) to split the string.

I struggled with sorting as the language seems to have no sorting on any of it's peculiar data types so I implemented the simple and inefficient Bubble sort that luckily works here just fast enough.

However on the process I realised the sorting is builtin feature of the versatile multidimensional array data type:

All siblings are automatically sorted numerically or alphabetically as they are added to the tree.

Because you can freely define your array indices I have effectively started using one-based array types. This very nicely eliminates those off-by-one errors I frequently have with languages with zero-based indexing.

Day 2

Algorithm part of the puzzle was still easy for me and I scored both parts on the first try. However I fell on the same trap than the most and I had to rewrite my part 1 solution to be more general so it could be applied also to part 2.

Here I had to take a next deep dive on the features of multidimensional array, especially on merge and kill commands. This was probably the first time I had to use these functions in the actual code. I'm starting to see the point of multidimensional array thought it is well hidden behind obscure syntax and fragmented documentation.

Oh, and I had to implement string split as the platform doesn't have it but only more primitive string operations:

ClassMethod splitInputLine(
    line As %String,
    Output levels)
{
    kill levels
    for i=1:1:$length(line," ") {
        set levels(i) = $piece(line," ",i)
    }
    set levels("size") = i // feeling clever!
}

Note the clever use of subscripts!

Day 3

So far there has been no need for computer science stuff but logical reasoning has been enough.

No new ObjectScript tricks today but maybe I should put one or two potentially reusable class (i.e. static) methods into a dedicated package in the spirit of over-engineering!

Day 4

I struggled in part 1 to build the diagonals - that was a huge time sink. I also had to study the %Regex.Matcher a bit more to get the matching right.

Part 2 was a breeze compared to part 1 but I made a typo and submitted wrong answer first (first time this year).

Day 5

In the first look to the example data the puzzle looked like a case for topological sort but it turned out the validation of updates in part 1 was not a sorting problem.

But then in part 2 topological sort was indeed needed. I have used topological sort only once before (in another Advent of Code puzzle) in my career and never with ObjectScript. I implemented it in a companion class (aoc.topo) and at the same time I added also two other small helpers with better ergonomics: aoc.list and aoc.str.

I also found a valid use case for goto command too!

Overall all this took some much time that this year day 5 was the point I fell behind.

Day 6

I had no time for the puzzles in three days so now I have permanently fallen behind the daily schedule.

This is the first case where the data have to be processed as 2d array so I had to make a few new preparations. I also created two new classes:

  • aoc.Coord - to help me passing and calculating new coordinates (positions)
  • aoc24.Guard - simulating the guard's movement

Part 1 was straightforward but I struggled a bit in part 2. However it didn't took too much time to figure out the following solution:

  1. reduce the search space to the visited positions only (65% reduction)
  2. set an obstackle in every visited positions (except the guard's initial position)
  3. simulate guard's movement as in part 1
    1. there is a loop if the guard hits the obstacle second time from the same direction
    2. there is no loop if the guard walks out of the area in the same way than in part 1

In my hardware the part 2 took ~90 seconds so clearly this is non-optimal brute force algorithm. One optimization idea (search space reduction) not implemented is to set the guard's initial position next to the new obstacle.

In the beginning I tried to check if there is a certain obstacle patterns that will lead to the loop but as I couldn't figure them out immediately I abandoned the approach.

Day 7

Part 1

I bet there is a mathematical number theory that solves this problem but with my limited mathematical skills I just started to calculate all possible equations and discard the equation when it's value exceeded the expected value. I googled "parsing mathematical expressions" for implementation ideas and ended maybe a bit over-complex solution to use stacks to save the current operation and it's operands. In every iteration of equation numbers I had two new stacks for holding add and multiply operators. If the value exceeded the expected value I discarded the stack, otherwise the stack was kept for next iteration. Essentially the stack is just an elaborate accumulator.

My stack class is aoc.stack. I was also interested to correctly record the execution time so enter aoc.StopWatch. (I really need to pay attention to the de-facto standard naming convention.)

In this puzzle I had a lot of silly mistakes, e.g. at one point I forgot to rename one variable, that took far too long to troubleshoot.

Part 2

The part 2 introduced a new operation and so the search space exploded as it very often does here. Occasionally this might render brute force approaches more or less "useless". For sure it's an indication there have to be a more clever approach. Anyway I didn't have any better ideas than to run through all the valid combinations as in part 1.

During the next weeks I had zero effort on Advent Of Code. Just on the New Year's Eve I decided to out to the internet for the solutions, run into a recursive Python solution in 15 seconds and decided go no further in the lands of computer science nor mathematics. Essentially I just had to add the concatenation operation exactly the same way I already used for addition. Yes, the execution time exploded but was still just 70 seconds (2 secs in part 1) in my hardware.

I scored the star on the first try.

Later I refactored the code so that the both parts use the same solution helper methods.

1 Comment
Discussion (1)1
Log in or sign up to continue
Announcement
· Dec 31, 2024

InterSystems Developer Ecosystem News, Q4 2024

Hello and welcome to the Developer Ecosystem News, Fourth Quarter!

This last quarter of the year was full of exciting activities in the InterSystems Developer Ecosystem. In case you missed something, we've prepared for you a selection of the hottest news and topics to catch up on!

News

🎇 Developer Community AI: InterSystems IRIS Documentation added

🎆 20,000 members on the InterSystems Developer Community!

🎆 400+ Ideas on the InterSystems Ideas Portal

🎇 Global Masters is back! Launch Date: October 3, 2024

🎊 2024 in Review: Celebrate Your Year with Developer Community!

🎊 Season's Greetings from the Developer Community!

🎊 Global Masters Holiday Cards

💡 InterSystems Ideas News #16 and #17

📝 Notifications & Co-Developers: New on Open Exchange!

📝 Faster Vector Searches with Approximate Nearest Neighbor index -- now available in the Vector Search Early Access Program

📝 InterSystems System Alerting and Monitoring (SAM) to Be Removed

📝 Production Decomposition - CCR Now Allows Source Controlling Individual Production Items

📝 InterSystems Package Manager (IPM) v0.9.0 Beta

👨‍🏫 InterSystems IRIS SQL Specialist Exam is now LIVE!

Contests & Events

 
InterSystems Tech Video Challenge
 
InterSystems Walking Challenge

🏆 First French Technical Article Contest

🏆 The 2nd InterSystems Japan Technical Writing Contest!

🏆 InterSystems IRIS Technical Tutorial Contest in Portuguese

👨‍💻 InterSystems at HackMIT 2024

👨‍💻 InterSystems at Hack Jak Brno Healthcare Hackathon 2024

🤝 Developer Meetup: Innovating on Health Data [GenAI, FHIR, Data Quality]

🤝 Developer Meetup on Security - October 15, Cambridge, MA

🤝 Developer Meetup on Gen AI, RAG, Hallucinations [Nov 19, Cambridge, MA]

🤝 Japan Developer Community Meetup in Tokyo

🎓 InterSystems DACH Symposium 2024

📺 Webinar in Spanish: ‘Facial recognition applied to application login using JavaScript+IRIS’

📺 Webinar in Spanish: Unifying Data with InterSystems IRIS: Exploring the 'Connect or Collect' Paradigm

📺 [Webinar] Are you and your data AI ready?

📺 Webinar with the winners of the InterSystems IRIS Technical Tutorials Contest

🌐 [LinkedIn Live] Debunking AI Myths with Expert Insights

Latest Releases

⬇️ General Availability of InterSystems IRIS, InterSystems IRIS for Health, and HealthShare Health Connect 2024.3

⬇️ Maintenance Releases 2023.1.5 & 2024.1.2 of InterSystems IRIS, IRIS for Health, & HealthShare HealthConnect are now available

⬇️ First InterSystems IRIS, InterSystems IRIS for Health, and HealthShare Health Connect 2025.1 Developer Preview available

⬇️ IPM 0.9.0 Released

Best Practices & Key Questions

❓ Key Questions: September, October, November, December

People and Companies to Know About 

👨‍💻 Celebrating a Star of the Developer Community

👨‍💻 Celebrating a Journey of Dedication

💼 Remote: Strong InterSystems HL7, ADT, CCDA to FHIR + IRIS + GCP

💼 IRIS InterSystems Engineer

💼 IRIS experience at CVS Healthcare

So...

Here is our take on the most interesting and important things! 

What were your highlights from this past quarter? Share them in the comments section and let's remember the fun we've had!

1 Comment
Discussion (1)3
Log in or sign up to continue
Article
· Dec 31, 2024 8m read

Creating a REST client to get Tracks from Spotify REST API - Part4 Save the Search Result

Last Chapter: Creating a REST client to get Tracks from Spotify REST API - Part3 Get some data (e.g. Artists)

Git link: https://github.com/ecelg/InterSystems-IRIS-as-a-Spotify-REST-client

 

OK we create a method to get data and lets try to get some Tracks 😁

Now open a terminal and test the code

Run the following line

w ##class(rest.utli.requestUtli).getdata("Spotify","/search","offset=5&limit=10&query=Shape%20of%20you&type=track&market=SG")

ooooo no seems there is huge among of data returns.....😥

I would like to know what information can be found in 1 track....🤔 how about only query 1 track?

let's try the following

w ##class(rest.utli.requestUtli).getdata("Spotify","/tracks/5cy3CNTBZbX8vZUIsu4p7K","market=SG")

Ok... there are quite a lot of information about a track...🤔... but I am pretty sure I don't need them all....🤔

maybe name, spotify url, album basic information and artists basic information are good enough for me....  just make it simple is ok

the hierarchy of the 3 object should be like this...

 

So... let's go

 


1. Build the object class to store the Artists information

create a spotify folder under rest

create a class  artists.cls

Create a Persistent object and extend it with the %JSON.Adaptor for generating JSON string in a convenience way.

you can use a different key field name of the JSON  string by adding the setting %JSONFIELDNAME

Class rest.spotify.artists Extends (%Persistent, %JSON.Adaptor)
{

Property name As %String;
Property spotifyid As %String(%JSONFIELDNAME = "id");
Property spotifyurl As %String(%JSONFIELDNAME = "spotify", MAXLEN = 100);
ClassMethod checkRowid(spotifyid As %String = "") As %Integer [ Language = objectscript ]
{
	//w ##class(rest.spotify.artists).checkRowid("6eUKZXaKkcviH0Ku9w2n3V")
	set rtn=0
	set rowid=""
	&sql(select id into :rowid from rest_spotify.artists where spotifyid=:spotifyid )
	//w rowid,!
	if rowid'="" set rtn=rowid
	return rtn
}
}

Save the class

 


2. Build the object class to store the Album information

under the spotify folder create a class album.cls

Create a Persistent object and extend it with the %JSON.Adaptor

Class rest.spotify.album Extends (%Persistent, %JSON.Adaptor)
{

Property name As %String;
Property albumtype As %String(%JSONFIELDNAME = "album_type");
Property releasedate As %String(%JSONFIELDNAME = "release_date");
Property spotifyid As %String(%JSONFIELDNAME = "id");
Property spotifyurl As %String(%JSONFIELDNAME = "spotify", MAXLEN = 100);
Property artists As artists;
ClassMethod checkRowid(spotifyid As %String = "") As %Integer [ Language = objectscript ]
{
	//w ##class(rest.spotify.album).checkRowid("6rUYFVeZl5rExNNV8seV98")
	set rtn=0
	set rowid=""
	&sql(select id into :rowid from rest_spotify.album where spotifyid=:spotifyid )
	//w rowid,!
	if rowid'="" set rtn=rowid
	return rtn
}
}

Save the class

 


3. Build the object class to store the Track information

under the spotify folder create a class track.cls

Create a Persistent object and extend it with the %JSON.Adaptor

Class rest.spotify.track Extends (%Persistent, %JSON.Adaptor)
{

Property name As %String;
Property popularity As %String;
Property tracknumber As %String(%JSONFIELDNAME = "track_number");
Property spotifyid As %String(%JSONFIELDNAME = "id");
Property spotifyurl As %String(%JSONFIELDNAME = "spotify", MAXLEN = 100);
Property album As album;
ClassMethod checkRowid(spotifyid As %String = "") As %Integer [ Language = objectscript ]
{
	//w ##class(rest.spotify.track).checkRowid("1cKuv7hAoX7ZN8f39hlDKX")
	set rtn=0
	set rowid=""
	&sql(select id into :rowid from rest_spotify.track where spotifyid=:spotifyid )
	//w rowid,!
	if rowid'="" set rtn=rowid
	return rtn
}

}

Save the class

 


4.  Now, we try to study what is the result look like if we try to search a track

Let's try to run the following command in the terminal again

w ##class(rest.utli.requestUtli).getdata("Spotify","/search","offset=5&limit=10&query=Shape%20of%20you&type=track&market=SG")

paste the result in a JSON fromatter for easy reading

Mummmm...🤨 mummmmm..............🤔

I think ... I need to write a function to loop through the the items of the result  JSON to get the tracks information and save it. We also need to save the album information inside the items.......😐........ and...... also.... loop through the artist information inside the items and save them........oooooooooooooooooooooo.... tones of work!!!

 


5. write some functions to loop though and save the search track result.

Create a class spotifyUtli.cls under the folder utli

write the following class methods

Class rest.utli.spotifyUtli Extends %RegisteredObject
{
ClassMethod getSearchResult(str As %String = "") As %Status [ Language = objectscript ]
{
	//w ##class(rest.utli.spotifyUtli).getSearchResult()
	set rtn=0
	set a={}.%FromJSON(str) //from json tn dynamic object
	//w a,!
	// loop through the object using an iterator
	set iter = a.%GetIterator()
	while iter.%GetNext(.key , .value ) 
	{
		//write !, ?5, "Key: ", key, ", Value: ", value, " type: ", a.%GetTypeOf(key)_" with value "_a.%Get(key)
		if key="tracks"
		{
			//w !,?10,"call get tracks",!
			set std=##class(rest.utli.spotifyUtli).getTrakSearchResult(.value)
		}
		//set b=value
		//w !,?10,b.name
	}
	
	kill a
	return rtn
}

ClassMethod getTrakSearchResult(ByRef tracksobj As %DynamicObject) As %Status [ Language = objectscript ]
{
	//w ##class(rest.utli.spotifyUtli).getTrakSearchResult()
	set rtn=0
	// loop through the object using an iterator
	set iter = tracksobj.%GetIterator()
	while iter.%GetNext(.key , .value ) 
	{
		//write !, ?5, "Key: ", key, ", Value: ", value
		if key="items"
		{
			// loop through the array using an iterator twice**
			set iter1=value.%GetIterator()
			while iter1.%GetNext(.key1 , .value1 )
			{
				kill tkobj
				set tkobj=##class(rest.spotify.track).%New()
				set rowid=""
				//write !, ?10, "Key: ", key1, ", Value: ", value1
				set iter2=value1.%GetIterator()
				while iter2.%GetNext(.key2 , .value2)
				{
					//write !, ?10, "Key: ", key2, ", Value: ", value2
					if key2="album"
					{
						//set std=##class(rest.utli.spotifyUtli).getTrakSearchAlbumResult(.value2) //w !,?15,"call get album"
						set album=##class(rest.spotify.album).%New()
						set std=##class(rest.utli.spotifyUtli).getTrakSearchAlbumResult(.value2,.album)
						set tkobj.album=album
						kill album
					}
					if key2="external_urls"
					{
						//w !,?15,value2.spotify
						set tkobj.spotifyurl=value2.spotify
					}
					if key2="name"
					{
						//w !,?15,"name: ",value2
						set tkobj.name=value2
					}
					if key2="popularity"
					{
						//w !,?15,"popularity: ",value2
						set tkobj.popularity=value2
					}
					if key2="track_number"
					{
						//w !,?15,"track_number: ",value2
						set tkobj.tracknumber=value2
					}
					if key2="id"
					{
						//w !,?15,"id: ",value2
						set tkobj.spotifyid=value2
						set rowid=##class(rest.spotify.track).checkRowid(value2)
					}
				}
				
				if (rowid=0)
				{
					set std=tkobj.%Save()
				}
				kill tkobj
			}
		}		
	}
	
	set rtn=1
	return rtn
}

ClassMethod getTrakSearchAlbumResult(ByRef albumobj As %DynamicObject, ByRef album As rest.spotify.album) As %Status [ Language = objectscript ]
{
	//w ##class(rest.utli.spotifyUtli).getTrakSearchAlbumResult()
	set rtn=0
	kill alobj
	set alobj=##class(rest.spotify.album).%New()
	set rowid=""
	// loop through the object using an iterator
	set iter = albumobj.%GetIterator()
	while iter.%GetNext(.key , .value ) 
	{
		//write !, ?5, "Key: ", key, ", Value: ", value
		if key="name"
		{
			//w !,?10,"name: ",value
			set alobj.name=value
			
		}
		if key="release_date"
		{
			//w !,?10,"release_date: ",value
			set alobj.releasedate=value
		}
		if key="id"
		{
			//w !,?10,"id: ",value
			set alobj.spotifyid=value
			set rowid=##class(rest.spotify.album).checkRowid(value)
		}
		if key="artists"
		{
			set artist=##class(rest.spotify.artists).%New()
			set std=##class(rest.utli.spotifyUtli).getTrakSearchArtistsResult(.value,.artist)
			set alobj.artists=artist
			kill artist
			//w !,?10,"call get artists"
		}
		if key="album_type"
		{
			//w !,?10,"album_type: ",value
			set alobj.albumtype=value
			
		}
		if key="external_urls"
		{
			//w !,?10,value.spotify
			set alobj.spotifyurl=value.spotify
		}
	}
	
	if (rowid=0)
	{
		set std=alobj.%Save()
		set album=alobj
	}
	if (rowid>0)
	{
		set album=##class(rest.spotify.album).%OpenId(rowid)
	}
	kill alobj
	
	set rtn=1
	return rtn
}

ClassMethod getTrakSearchArtistsResult(ByRef artistsobj As %DynamicObject, ByRef artist As rest.spotify.artists) As %Status [ Language = objectscript ]
{
	//w ##class(rest.utli.spotifyUtli).getTrakSearchArtistsResult()
	set rtn=0
	// loop through the array using an iterator twice**
	set iter = artistsobj.%GetIterator()
	while iter.%GetNext(.key , .value ) 
	{
		kill aobj
		set aobj=##class(rest.spotify.artists).%New()
		set rowid=""
		set iter1 = value.%GetIterator()
		while iter1.%GetNext(.key1 , .value1 ) 
		{
			//write !, ?5, "Key: ", key1, ", Value: ", value1
			if key1="id"
			{
				set aobj.spotifyid=value1
				set rowid=##class(rest.spotify.artists).checkRowid(value1)
			}
			if key1="name"
			{
				//w !,?12,"name: ",value1
				set aobj.name=value1
			}
			if key1="external_urls"
			{
				//w !,?12,value1.spotify
				set aobj.spotifyurl=value1.spotify
			}
		}
		
		if (rowid=0)
		{
			set std=aobj.%Save()
			set artist=aobj
		}
		if (rowid>0)
		{
			set artist=##class(rest.spotify.artists).%OpenId(rowid)
		}
		
		kill aobj
	}
	
	set rtn=1
	return rtn
}
}

Save the class

 


6. Test the class method getSearchResult

Run the following line

w ##class(rest.utli.spotifyUtli).getSearchResult(##class(rest.utli.requestUtli).getdata("Spotify","/search","offset=5&limit=10&query=Shape%20of%20you&type=track&market=SG"))

Seems ok.... so far....😀

 


7. Check the data from the table

 

a. check the artist by the following SQL

SELECT
ID, name, spotifyid, spotifyurl
FROM rest_spotify.artists

b. Check the album by the following SQL

SELECT
ID, albumtype, artists->name as artist, name, releasedate, spotifyid, spotifyurl
FROM rest_spotify.album

 

c. Check the track by the following SQL

SELECT
ID, album->name as album, album->artists->name as artist, name, popularity, spotifyid, spotifyurl, tracknumber
FROM rest_spotify.track

 

Yeah!!!! Working!!! I can create my track list by querying Spotify now!!!!!!!!😂🤣😉

Discussion (0)1
Log in or sign up to continue
Question
· Dec 31, 2024

How to Connect Laravel 11 with InterSystems IRIS Data Platform via ODBC?

Hi everyone,

I'm trying to connect my Laravel application with the InterSystems IRIS Data Platform using ODBC.I need help setting up the ODBC connection and querying the database properly. What is the proper way to configure Laravel to work with an ODBC connection to InterSystems IRIS?

Any help would be greatly appreciated!

Thanks in advance!

1 Comment
Discussion (1)1
Log in or sign up to continue
Question
· Dec 30, 2024

Unique Index Open on Empty String

I have a unique index on two properties in a class, and I want to open the instance by index key with one of the two properties is set to the empty string. Is this possible to perform?

Take the example class here

Class TestIndex
{

Property Mandatory as TestClass [Required];
Property Alternate as TestClass;
Property AltOrEmpty As %String [ Calculated, Required, SqlComputeCode = {Set {*} = $Case({Alternate},"":$c(0),:{Alternate})}, SqlComputed ];
Index AlternateMandatory on (Mandatory, AltOrEmpty) [ Unique ];
}

And example data as

Mandatory Alternate AltOrNUL
2    
2 3 3

I can open the second line in the table as 

>set testInstance = ##class(TestIndex).AlternateMandatoryOpen(2,3,.st)
>w st
1

And I want to be able to use the same functionality to open the first line. This would be as open on one valid Id and on empty string, which results in an attempt to open NULL and fails. 

>set testInstance = ##class(TestIndex).AlternateMandatoryOpen(2,"",.st)
>zw st
ERROR #5770: Object open failed because 'AlternateMandatory' key value of '2:NULL' was not found

I am wondering if there is a syntax that would work to find and open this instance based on the unique index containing the empty string, or not based on the inherent design to have a unique index on an empty string.

2 Comments
Discussion (2)2
Log in or sign up to continue