Storing Plugin Data Revisited (Wall Presets)

I want to address the wall preset issue that has been dogging me for way too long.

My idea has always been to make the storage system for the presets manually editable by the user (if required) so I have simply stored the data as long strings of text in a simple text file (.txt)

Here is an example of a bunch of wall presets:

Medeek Wall Preset File
2|Stucco Rockwool|20210510115553|Int-Ext|Front|97.125|80.0|5.5|1.5|16.0|Left|0.0|2|1.5|1|1.5|NO|YES|Outside Corner|90.0|1.5|1|California|Stud Depth|YES|Outside Corner|90.0|1.5|1|California|Stud Depth|YES|YES|0.4375|FLUSH|YES|0.25|0.0|MITER|YES|5.0|NO|YES|0.0|0.0|0.0|0.0|0.0|ZIP_12|STUCCO_LIGHT_TAN|WALLGYPSUM|RKW|YES|NO|NO|NO|NO|0.5|WALLGYPSUM|NO|0.4375|FLUSH|ZIP_12|Center|YES|1|48.0|ON SHEATHING|0.75|YES|3.5|YES|3.5|YES|#ffffff|YES|3.5|0.75|NO|48.0|YES|5.5|0.75|NO|YES|7.25|0.75|YES|YES|0.75|1.25|36.0|4.0|1.0|MITER|BRICK_RED|YES|4.0|1.0|YES|0.0|0.0|MODE1|BRICK_RED|A|16.0|12.0|12.0|2.0|1.0|QUOIN
3|Stucco Pink|20210510124401|Int-Ext|Front|97.125|80.0|5.5|1.5|16.0|Left|0.0|2|1.5|1|1.5|NO|YES|Outside Corner|90.0|1.5|1|California|Stud Depth|YES|Outside Corner|90.0|1.5|1|California|Stud Depth|YES|YES|0.4375|FLUSH|YES|0.25|0.0|MITER|YES|0.5|YES|NO|0.0|0.0|0.0|0.0|0.0|OSB_FELT|STUCCO_LIGHT_TAN|WALLGYPSUM|PFG|NO|NO|NO|NO|NO|0.5|WALLGYPSUM|NO|0.4375|FLUSH|OSB_FELT|HDU8|0.0|SB78-24|24.0|BOTH|FRAMING|Center|YES|1|48.0|ON SHEATHING|0.75|YES|3.5|YES|3.5|YES|#ffffff|YES|3.5|0.75|NO|48.0|YES|5.5|0.75|NO|YES|7.25|0.75|YES|YES|0.75|1.25|36.0|4.0|1.0|MITER|BRICK_RED|YES|4.0|1.0|YES|0.0|0.0|MODE1|BRICK_RED|A|16.0|12.0|12.0|2.0|1.0|QUOIN
4|test2|20210510145752|Int-Ext|Front|97.125|80.0|5.5|1.5|16.0|Left|0.0|2|1.5|1|1.5|NO|YES|Outside Corner|90.0|1.5|1|California|Stud Depth|YES|Outside Corner|90.0|1.5|1|California|Stud Depth|YES|YES|0.4375|FLUSH|YES|0.25|0.0|MITER|YES|0.5|NO|NO|0.0|0.0|0.0|0.0|0.0|OSB|HARDI_CM_HM|WALLGYPSUM|PFG|NO|NO|NO|NO|NO|0.5|WALLGYPSUM|NO|0.4375|FLUSH|OSB|HDU8|0.0|SB78-24|24.0|BOTH|FRAMING|Center|YES|1|48.0|ON SHEATHING|0.75|YES|3.5|YES|3.5|YES|#ffffff|YES|3.5|0.75|NO|48.0|YES|5.5|0.75|NO|YES|7.25|0.75|YES|YES|0.75|1.25|36.0|4.0|1.0|MITER|BRICK_RED|YES|4.0|1.0|YES|0.0|0.0|MODE1|BRICK_RED|A|16.0|12.0|12.0|2.0|1.0|QUOIN
5|4/2 Wall|20210510145837|Int-Ext|Front|97.125|80.0|5.5|1.5|16.0|Left|0.0|2|1.5|1|1.5|NO|YES|Outside Corner|90.0|1.5|1|California|Stud Depth|YES|Outside Corner|90.0|1.5|1|California|Stud Depth|YES|YES|0.4375|FLUSH|YES|0.25|0.0|MITER|YES|0.5|NO|NO|0.0|0.0|0.0|0.0|0.0|OSB|HARDI_CM_HM|WALLGYPSUM|PFG|NO|NO|NO|NO|NO|0.5|WALLGYPSUM|NO|0.4375|FLUSH|OSB|HDU8|0.0|SB78-24|24.0|BOTH|FRAMING|Center|YES|1|48.0|ON SHEATHING|0.75|YES|3.5|YES|3.5|YES|#ffffff|YES|3.5|0.75|NO|48.0|YES|5.5|0.75|NO|YES|7.25|0.75|YES|YES|0.75|1.25|36.0|4.0|1.0|MITER|BRICK_RED|YES|4.0|1.0|YES|0.0|0.0|MODE1|BRICK_RED|A|16.0|12.0|12.0|2.0|1.0|QUOIN
6|120wall|20210606121902|Int-Ext|Front|120.0|80.0|5.5|1.5|16.0|Left|0.0|2|1.5|1|1.5|NO|YES|Outside Corner|90.0|1.5|1|California|Stud Depth|YES|Outside Corner|90.0|1.5|1|California|Stud Depth|YES|YES|0.4375|FLUSH|YES|0.25|0.0|MITER|YES|0.5|NO|NO|0.0|0.0|0.0|0.0|0.0|OSB|HARDI_CM_HM|WALLGYPSUM|PFG|NO|NO|NO|NO|NO|0.5|WALLGYPSUM|NO|0.4375|FLUSH|OSB|HDU8|0.0|SB78-24|24.0|BOTH|FRAMING|Center|YES|1|48.0|ON SHEATHING|0.75|YES|3.5|YES|3.5|YES|#ffffff|YES|3.5|0.75|NO|48.0|YES|5.5|0.75|NO|YES|7.25|0.75|YES|YES|0.75|1.25|36.0|4.0|1.0|MITER|BRICK_RED|YES|4.0|1.0|YES|0.0|0.0|MODE1|BRICK_RED|A|16.0|12.0|12.0|2.0|1.0|QUOIN
7|150HD|20210606122002|Int-Ext|Front|150.0|80.0|5.5|1.5|16.0|Left|0.0|2|1.5|1|1.5|NO|YES|Outside Corner|90.0|1.5|1|California|Stud Depth|YES|Outside Corner|90.0|1.5|1|California|Stud Depth|YES|YES|0.4375|FLUSH|YES|0.25|0.0|MITER|YES|0.5|NO|YES|0.0|0.0|0.0|0.0|0.0|OSB|HARDI_CM_HM|WALLGYPSUM|PFG|NO|NO|NO|NO|NO|0.5|WALLGYPSUM|NO|0.4375|FLUSH|OSB|HDU8|0.0|SB78-24|24.0|BOTH|FRAMING|Center|YES|1|48.0|ON SHEATHING|0.75|YES|3.5|YES|3.5|YES|#ffffff|YES|3.5|0.75|NO|48.0|YES|5.5|0.75|NO|YES|7.25|0.75|YES|YES|0.75|1.25|36.0|4.0|1.0|MITER|BRICK_RED|YES|4.0|1.0|YES|0.0|0.0|MODE1|BRICK_RED|A|16.0|12.0|12.0|2.0|1.0|QUOIN
8|150BLKHD|20210606122102|Int-Ext|Front|150.0|80.0|5.5|1.5|16.0|Left|0.0|2|1.5|1|1.5|NO|YES|Outside Corner|90.0|1.5|1|California|Stud Depth|YES|Outside Corner|90.0|1.5|1|California|Stud Depth|YES|YES|0.4375|FLUSH|YES|0.25|0.0|MITER|YES|0.5|NO|YES|0.0|0.0|0.0|0.0|0.0|OSB|HARDI_CM_HM|WALLGYPSUM|PFG|YES|NO|NO|NO|NO|0.5|WALLGYPSUM|NO|0.4375|FLUSH|OSB|HDU8|0.0|SB78-24|24.0|BOTH|FRAMING|Center|YES|1|48.0|ON SHEATHING|0.75|YES|3.5|YES|3.5|YES|#ffffff|YES|3.5|0.75|NO|48.0|YES|5.5|0.75|NO|YES|7.25|0.75|YES|YES|0.75|1.25|36.0|4.0|1.0|MITER|BRICK_RED|YES|4.0|1.0|YES|0.0|0.0|MODE1|BRICK_RED|A|16.0|12.0|12.0|2.0|1.0|QUOIN

Notice how I’ve used the vertical bar to separate the values/parameters, any deliminator could be used but the vertical bar is not something one would use typically in any of the fields as values so it makes for a convenient choice.

This system works well enough for storing and retrieving the data however the huge downside is that it is an ordered array of values and if I add in a new parameter (somewhere in the middle) then it throws off previous versions of preset files (ie. no backward compatibility).

I need a better way or method of storing this data. It can’t be binary since it needs to be visible to the user but it needs to be either some sort of structure, hash or XML so that I can easily add additional parameters to the system when required.

Any suggestions?

Here is an interesting thread on the matter from a couple years back:

Below is a small chunk of code that reads in my current wall presets, a very simple system really but it is time to move to a more advanced hash system:

@Presetfilename = File.join(APPDATA_MEDEEK_WALL,"presets/WALL_PRESETS.txt")

	if (File.exist?(@Presetfilename))

		rowarray = preset_to_load.strip.split(',')
		presetnum = rowarray[0]
		presetname = rowarray[1]

		# puts "#{presetnum} #{presetname}"

		#############################################
		#
		# Reads preset file and finds wall to load

		datastring = ''
		foundkey = 0

        	begin
          		File.open(@Presetfilename, 'r') { |file|
            			file.each_line("\n") do |row|
					# if row =~ /^#{presetnum}\|#{Regexp.escape(presetname)}\|/
					if row =~ /^#{presetnum}\|#{presetname}\|/
						datastring << "#{row}"
						foundkey = 1
						break
					end

            			end
          		}

        	rescue StandardError => e
          		UI.messagebox(e)
          		puts "Error Message: #{e}"
        	end


		if foundkey == 1

			#############################################
			#
			# Dumps preset string into class variables

			arry = datastring.strip.split('|')

			# Basic Options

			@Presetnum_p		= 	arry[0].to_i
			@Presetname_p 		= 	arry[1]
			@Presetdate_p		= 	arry[2]
			@Walltype_p 		= 	arry[3]
			@Walljust_p		= 	arry[4]
			@Wallhgt_p		= 	arry[5].to_f
			@Wallhdrhgt_p		= 	arry[6].to_f
			@Stud_depth_p		= 	arry[7].to_f
			@Stud_width_p		= 	arry[8].to_f
			@Studspacing_p		= 	arry[9].to_f
			@Studdir_p		= 	arry[10]
			@Std_offset_p		=	arry[11].to_f
			@Plt_top_num_p		= 	arry[12]
			@Plt_top_p		= 	arry[13].to_f
			@Plt_btm_num_p		= 	arry[14]
			@Plt_btm_p		= 	arry[15].to_f
			@Plt_btm_pt_p		= 	arry[16]
			@Advwalloptions_p	= 	arry[17]

			# Corner Options

			@Corner_start_p		= 	arry[18]
			@Cornerangle1_p		= 	arry[19]
			@Cornerstudthk1_p	= 	arry[20].to_f
			@Cornerstudqty1_p	= 	arry[21]
			@Cornertreatment1_p	= 	arry[22]
			@Corneroffset1_p	= 	arry[23]
			@Cornertr1_p		=	arry[24]

			@Corner_end_p		= 	arry[25]
			@Cornerangle2_p		= 	arry[26]
			@Cornerstudthk2_p	= 	arry[27].to_f
			@Cornerstudqty2_p	= 	arry[28]
			@Cornertreatment2_p	= 	arry[29]
			@Corneroffset2_p	= 	arry[30]
			@Cornertr2_p		=	arry[31]

			# Advanced Options

			@Wallsheathoption_p	= 	arry[32]
			@Wallsheaththk_p	= 	arry[33].to_f
			@Wallsheathcorner_p	= 	arry[34]
			@Wallcladoption_p	= 	arry[35]
			@Wallcladthk_p		= 	arry[36].to_f
			@Wallcladgap_p		= 	arry[37].to_f
			@Wallcladcorner_p	= 	arry[38]
			@Wallgypsumoption_p	= 	arry[39]
			@Wallgypsumthk_p	= 	arry[40].to_f
			@Wallinsuloption_p	= 	arry[41]
			@Holdownoption_p	= 	arry[42]
			@Wallsheathvertoffset_t_p	= 	arry[43].to_f
			@Wallsheathvertoffset_b_p	= 	arry[44].to_f
			@Wallcladvertoffset_t_p		= 	arry[45].to_f
			@Wallcladvertoffset_b_p		= 	arry[46].to_f
			@Clad_mat_offset_p	=	arry[47].to_f
			@Clad_mat_hoffset_p	=	arry[48].to_f


			@Wallsheathmat_p	= 	arry[49]
			@Wallcladmat_p		= 	arry[50]
			@Wallgypsummat_p	= 	arry[51]
			@Wallinsulmat_p		= 	arry[52]
			@Blockingoption_p	= 	arry[53]
			@Trimoption_p		= 	arry[54]
			@Wsct_option_p		=	arry[55]
			@Quoin_option_p		=	arry[56]

			@Wallgypsum2option_p	= 	arry[57]
			@Wallgypsum2thk_p	= 	arry[58].to_f
			@Wallgypsum2mat_p	= 	arry[59]

			@Wallsheath2option_p	= 	arry[60]
			@Wallsheath2thk_p	= 	arry[61].to_f
			@Wallsheath2corner_p	= 	arry[62]
			@Wallsheath2mat_p	= 	arry[63]

			# Holdown Options
			
			@Holdowntype_p		= 	arry[64]
			@Holdownvertoffset_p	= 	arry[65].to_f
			@Holdown_ab_p		= 	arry[66]
			@Thd_rod_length_p	= 	arry[67].to_f
			@Holdown_location_p	= 	arry[68]
			@Holdown_strap_loc_p	= 	arry[69]

			# Blocking Options

			@Blockinghgt_p		= 	arry[70]
			@Staggerblocking_p	= 	arry[71]
			@Blockingnum_p		= 	arry[72].to_i
			@Blockingspacing_p	= 	arry[73].to_f

			# Trim Options

			@Trimloc_p		= 	arry[74]
			@Trimthk_p		= 	arry[75].to_f
			@Trimoutoption_p	= 	arry[76]
			@Trimwidthout_p		= 	arry[77].to_f
			@Triminoption_p		= 	arry[78]
			@Trimwidthin_p		= 	arry[79].to_f
			@Trimcut_p		= 	arry[80]
			@Walltrimmat_p		=	arry[81]

			@Trim_band_p		=	arry[82]
			@Trim_band_width_p	=	arry[83].to_f
			@Trim_band_thk_p	=	arry[84].to_f
			@Trim_band_cont_p	=	arry[85]
			@Trim_band_hgt_p	=	arry[86].to_f

			@Trim_frieze_p		=	arry[87]
			@Trim_frieze_width_p	=	arry[88].to_f
			@Trim_frieze_thk_p	=	arry[89].to_f
			@Trim_frieze_cont_p	=	arry[90]

			@Trim_skirt_p		=	arry[91]
			@Trim_skirt_width_p	=	arry[92].to_f
			@Trim_skirt_thk_p	=	arry[93].to_f
			@Trim_skirt_cont_p	=	arry[94]

			@Trim_cap_p		=	arry[95]
			@Trim_cap_height_p	=	arry[96].to_f
			@Trim_cap_depth_p	=	arry[97].to_f

			# Wainscot Options

			@Wsct_hgt_p		= 	arry[98].to_f
			@Wsct_thk_p		= 	arry[99].to_f
			@Wsct_gap_p		= 	arry[100].to_f
			@Wsct_corner_p		= 	arry[101]
			@Wsct_mat_p		= 	arry[102]
			@Wsct_ledge_p		= 	arry[103]
			@Wsct_ledgeheight_p	= 	arry[104].to_f
			@Wsct_ledgedepth_p	=	arry[105].to_f
			@Wsct_ledgewin_p	=	arry[106]

			@Wsct_start_p		=	arry[107].to_f
			@Wsct_end_p		=	arry[108].to_f
			@Wsct_mode_p		=	arry[109]
			@Wsct_ledgemat_p	= 	arry[110]

			# Quoin Options

			@Q_style_p		= 	arry[111]
			@Q_l1_p			= 	arry[112].to_f
			@Q_l2_p			= 	arry[113].to_f
			@Q_hgt_p		= 	arry[114].to_f
			@Q_spc_p		= 	arry[115].to_f
			@Q_thk_p		= 	arry[116].to_f
			@Q_mat_p		= 	arry[117]
			
		end

	else
		#######################################
		#
		# Creates new preset file and folder

		
		presetfoldername = File.join(APPDATA_MEDEEK_WALL,"presets")

		unless Dir.exist?(presetfoldername)

			puts "Creating Medeek Presets Folder: #{presetfoldername}"

			begin
				Dir.mkdir(presetfoldername)
			rescue StandardError => e
      				UI.messagebox(e)
				puts "Error Message: #{e}"
    			end
		end


		begin
			File.open(@Presetfilename, 'w') { |file|
				file.puts "#{MDK_WALL} Preset File\n"

			}
		rescue StandardError => e
      			UI.messagebox(e)
			puts "Error Message: #{e}"
    		end


	end

Here is my method for saving a preset:

def save_wall_preset (preset_to_save, preset_string)


	@Presetfilename = File.join(APPDATA_MEDEEK_WALL,"presets/WALL_PRESETS.txt")

	if (File.exist?(@Presetfilename))


		#############################################
		#
		# Reads preset file and finds if wall preset already exists

		newdatastring = ''
		foundkey = 0
		presetnumlast = 0
		enum_preset = ''

        	begin
          		File.open(@Presetfilename, 'r') { |file|
            			file.each_line("\n") do |row|
					# if row =~ /^#{presetnum}\|#{Regexp.escape(presetname)}\|/
					if row =~ /^\d+\|#{preset_to_save}\|/
						# Copy over existing preset with new preset
						# puts "found key:  #{preset_string}"
	
						rowarray = row.strip.split('|')
						presetnumi = rowarray[0]

						newrow = presetnumi.to_s + '|' + preset_string
						newdatastring << "#{newrow}\n"
					
						enum_preset = presetnumi.to_s + ',' + preset_to_save
						foundkey = 1
					else
						rowarray = row.strip.split('|')
						presetnumi = rowarray[0].to_i
						if presetnumi > presetnumlast
							presetnumlast = presetnumi
						end

						newdatastring << "#{row}"
					end

            			end
          		}

        	rescue StandardError => e
          		UI.messagebox(e)
          		puts "Error Message: #{e}"
        	end


		if foundkey == 1

			# Do nothing
		else
			presetnumlast = presetnumlast + 1
			newrow = presetnumlast.to_s + '|' + preset_string
			newdatastring << "#{newrow}\n"

			enum_preset = presetnumlast.to_s + ',' + preset_to_save
			
		end
		
		#############################################
		#
		# Writes updated datastring into preset file

		begin
      			File.open(@Presetfilename, 'w') { |file|
            			file.puts newdatastring
      			}
    		rescue StandardError => e
      			UI.messagebox(e)
			puts "Error Message: #{e}"
    		end	

		return enum_preset

	else
		#######################################
		#
		# Creates new preset file and folder

		
		presetfoldername = File.join(APPDATA_MEDEEK_WALL,"presets")

		unless Dir.exist?(presetfoldername)

			puts "Creating Medeek Presets Folder: #{presetfoldername}"

			begin
				Dir.mkdir(presetfoldername)
			rescue StandardError => e
      				UI.messagebox(e)
				puts "Error Message: #{e}"
    			end
		end


		begin
			File.open(@Presetfilename, 'w') { |file|
				file.puts "#{MDK_WALL} Preset File\n"

			}
		rescue StandardError => e
      			UI.messagebox(e)
			puts "Error Message: #{e}"
    		end

	end


end

Creating the string to save as the preset is done like the following:

@preset_string = @preset_to_save + '|' + formatted_time + '|' + walltype + '|' + walljust + '|' + wallhgt.to_s + '|' + wallhdrhgt.to_s + '|' + stud_depth.to_s + '|' + stud_width.to_s + '|' + studspacing.to_s + '|' + studdir + '|' + std_offset.to_s + '|' + plt_top_num.to_s + '|' + plt_top.to_s + '|' + plt_btm_num.to_s + '|' + plt_btm.to_s + '|' + plt_btm_pt + '|' + advwalloptions

I’m thinking I could probably preserve my overall system as is and just use a custom text method rather than JSON or some other more cumbersome system.

Essentially the first two parameters would remain the same (ie. preset number and preset name).

However all of the other entries (that are deliminated by the vertical bars) would change from a simple value to a key/value pair.

For example instead of

|Int-Ext|

I would change it to

|walltype=Int-Ext|

The equal sign would become the secondary deliminator. I can still store the entire string as a single line in my text files however it will become approximately double in length (no big deal).

I can then split the string into an array and then run a foreach on the array and turn it into a hash, from there it is just a matter of pulling data from the the hash.

Any thoughts on this plan and possible flaws I may have overlooked?

Why do you think JSON is cumbersome?

  • It can store all the common types of value
  • There is a standard Ruby library to generate and parse JSON, which makes it really easy to convert from arrays/hashes to JSON and back
  • You can add new parameters at any time without changing the format and invalidating older presets
  • It’s human-readable, so your user could even manually edit preset files

What you are describing with custom string generation and everything, seems much more cumbersome to me than using JSON. Don’t reinvent the wheel, JSON is made for this kind of usage.

4 Likes

I’ve looked at the JSON module and have given it some thought but I also like how compact my custom solution feels, but maybe I have grown too attached to it and a more standardized approach is warranted.

I use plain text json in my plugin. It is still pretty easy to read in a text editor, but much easier to parse.

Here you can see how I can handle some or all of my settings being missing.

@settings_path = ENV['GEM_PATH'].split('SketchUp')[0] + "#{extension_name}/"
  def read_settings
    settings_file = @settings_path + 'settings.json'
    if File.exist?(settings_file)
      begin
        json = File.open(settings_file, 'r', &:read)
        @settings = JSON.parse(json)
        restore_default_settings unless @settings
        restore_colors unless @settings.key?('colors')
        restore_default_colors unless @settings.key?('default_colors')
        restore_default_building_specs unless @settings.key?('default_building')
        restore_default_preferences unless @settings.key?('preferences')
        restore_default_trim_widths unless @settings.key?('trim_widths')
        restore_default_sb_preferences unless @settings.key?('sb_preferences')
        change_trim_widths(@settings['trim_widths'].to_json, false)
      rescue StandardError => _e
        restore_default_settings
      end
    else
      restore_default_settings
    end
    read_openings
  end

With json it is very easy handle situations where ‘old’ settings files are missing properties.

data['key'] ||= default_value
3 Likes

Also to preserve backward compatibility you could check the first character and if it is { then parse json, otherwise use your legacy system.

1 Like

I personally think json is much easier to work with even in plain text. Besides that it is the de facto standard so people already know how the format works.

Here is your example converted to json. I think the json is much more readable.

2|Stucco Rockwool|20210510115553|Int-Ext|Front|97.125|80.0|5.5|1.5|16.0|Left
{
  "Presetnum_p" : 2,
  "Presetname_p" : "Stucco Rockwool",
  "Presetdate_p" : 20210510115553,
  "Walltype_p" : "Int-Ext",
  "Walljust_p" : "Front",
  "Wallhgt_p" : 97.125,
  "Wallhdrhgt_p" : 80.0,
  "Stud_depth_p" : 5.5,	
  "Stud_width_p" : 1.5,
  "Studspacing_p" : 16,
  "Studdir_p" : "Left"
}
2 Likes

There is something to be said for the readability of the JSON. However, having each preset on a single line makes it very easy to manipulate the individual presets.

I suppose I could contain all of the presets in a nested array/hash structure but the size of this single JSON structure would be daunting.

I’m pretty sure this is valid JSON:

[
  {
    "Presetnum_p" : 2,
    "Presetname_p" : "Stucco Rockwool",
    "Presetdate_p" : 20210510115553,
    "Walltype_p" : "Int-Ext",
    "Walljust_p" : "Front",
    "Wallhgt_p" : 97.125,
    "Wallhdrhgt_p" : 80.0,
    "Stud_depth_p" : 5.5,	
    "Stud_width_p" : 1.5,
    "Studspacing_p" : 16,
    "Studdir_p" : "Left"
  },
  {
    "Presetnum_p" : 3,
    "Presetname_p" : "Stucco Pink",
    "Presetdate_p" : 20210510124401,
    "Walltype_p" : "Int-Ext",
    "Walljust_p" : "Front",
    "Wallhgt_p" : 97.125,
    "Wallhdrhgt_p" : 80.0,
    "Stud_depth_p" : 5.5,	
    "Stud_width_p" : 1.5,
    "Studspacing_p" : 16,
    "Studdir_p" : "Left"
  }
]

Parse this and you get an array that you can iterate.

Or you can even have an unordered hash referring to presets by their “num” or another field:

{
  "2": {
    "Presetnum_p" : 2,
    "Presetname_p" : "Stucco Rockwool",
    "Presetdate_p" : 20210510115553,
    "Walltype_p" : "Int-Ext",
    "Walljust_p" : "Front",
    "Wallhgt_p" : 97.125,
    "Wallhdrhgt_p" : 80.0,
    "Stud_depth_p" : 5.5,	
    "Stud_width_p" : 1.5,
    "Studspacing_p" : 16,
    "Studdir_p" : "Left"
  },
  "3": {
    "Presetnum_p" : 3,
    "Presetname_p" : "Stucco Pink",
    "Presetdate_p" : 20210510124401,
    "Walltype_p" : "Int-Ext",
    "Walljust_p" : "Front",
    "Wallhgt_p" : 97.125,
    "Wallhdrhgt_p" : 80.0,
    "Stud_depth_p" : 5.5,	
    "Stud_width_p" : 1.5,
    "Studspacing_p" : 16,
    "Studdir_p" : "Left"
  }
}

After parsing you can get a preset using presets["2"]

1 Like

Disclaimer: I’m formally trained in programming - 40 years ago! I’m NOT up on modern languages (like Ruby), nor am I a developer.

@Medeek: It seems to me that you’re clinging to your old method of storing presets for your convenience. From an extension user’s POV, I don’t care - except that it’s a major PITA to delete, then recreate presets when you add new settings! I should never have to look at - or worry about - how the presets are stored.

With that being said, from the glimpse of JSON I see in previous replies, and my own knowledge of XML, I’d say go with JSON. It’s more compact than XML, more readable, and (again from previous replies) there is a standard Ruby library for dealing with JSON.

When JSON is parsed, I’m guessing it creates a hash, where each piece of information is stored as a name/value pair. While you might consider storing your settings in a hash to be a PITA - as far as changing your existing code is concerned, you should only need to make that effort ONCE.- OK, and change your coding style going forward.

So I’m in agreement with @Jiminiybillybob - don’t reinvent the wheel!

1 Like

I’d also recommend JSON. It’s easy to use, human readable and widely supported.

This sounds like it could lead to issues if a string for whatever reason includes a |. Even if this isn’t the case today, maybe it could happen in the future. Maybe a user could enter it as custom text somewhere someday. Rather than choosing a rarely used character I’d recommend using a solid syntax that is guaranteed to work.

For instance CVS handles this by surrounding strings containing the delimiter with quotation marks, and using an escape characters when quotes or the escape character itself appears in a string. This may all sound very advanced, but the good thing is that people already have solved this and made libraries for it. This applies both to CVS and JSON.

Regarding compactness, a few bytes here or there makes no difference. This code is not stored on the Apollo computer or an early 1990s game console. There are cases where file size matters, like video, large textures, but control characters in a config file are fine.

2 Likes

One other reason for going with a “standard” format - be it JSON, XML, or something else:

Should you ever decide to offer an API - to allow other programmers/developers to access/understand/change your Sketchup entities, given them a standard way of reading/writing your settings would me a mikvah.

1 Like

+1 for JSON. It is a standard nowadays and it will have fixed issues you are not having yet with any custom solution.

1 Like

The general consensus seems to lean very heavily towards JSON… okay, I concede.

Now I just need to figure out how best to implement a JSON solution. Ultimately it will fix my backward compatibility problems when I decide to add additional parameters in the future, that is what is important.

I thought CVS mostly handle drugs and groceries :wink: (I think you meant CSV - Comma Separated Values)

2 Likes

Yea, I’m off to CVS in a bit to get a prescription myself.

I told you this quite some time ago.

And I remember (then) you had an aversion to using @options[keyname] references, and wanted to use named instance variables that indicated the meaning of the option.
BUT … you can still do this (@vars) using metaprogramming. Ie …

  # In your class' constructor ...
  def initialize(*args)
    # Load options into @options hash ...
    @options = options_load()
    # Set the instance variables ...
    @options.each do |key,value|
      self.instance_variable_set("@#{key}", value)
    end
  end
  def options_load
    json = File.read(OPTIONS_PATH, mode: "r", encoding: "UTF-8")
    JSON.parse(json)
  rescue => err
    puts "Error reading options file ..."
    puts err.inspect
  end

  def options_save
    json = @options.to_json
    File.write(OPTIONS_PATH, json, mode: "w", encoding: "UTF-8")
  rescue => err
    puts "Error writing options file ..."
    puts err.inspect
  end

  def options_update
    self.instance_variables.each do |var|
      # var will be a symbol beginning with an @ char:
      key = var.to_s[1..-1]
      @options[key]= self.instance_variable_get(var)
    end
  end

If the options will be the same for every instance … then @options might need to be a class constant.
It is not likely that you would change the reference to the options hash during the session. It is okay to modify hash values referenced by a constant. So in these examples, @options would become OPTIONS within the class.
Also, the options control methods would likely become class methods instead of instance methods. (You would need to pass the instance reference into the options_update method in order to read the instance variable values.)


BTW, @Neil_Burkholder your example does not close the json file. Should probably use IO::read (aka File::read) instead of File::open.

Thanks for pointing that out. Not sure why I didn’t use: json = File.read(settings_file)

1 Like