galaxy 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. data/.document +5 -0
  2. data/.gitignore +21 -0
  3. data/.idea/.rakeTasks +7 -0
  4. data/.idea/encodings.xml +5 -0
  5. data/.idea/galaxy.iml +10 -0
  6. data/.idea/misc.xml +14 -0
  7. data/.idea/modules.xml +9 -0
  8. data/.idea/vcs.xml +8 -0
  9. data/.idea/workspace.xml +486 -0
  10. data/LICENSE +20 -0
  11. data/README.rdoc +36 -0
  12. data/Rakefile +62 -0
  13. data/VERSION +1 -0
  14. data/doc/plan.txt +5 -0
  15. data/doc/pseudo.txt +43 -0
  16. data/features/galaxy.feature +9 -0
  17. data/features/step_definitions/galaxy_steps.rb +0 -0
  18. data/features/support/env.rb +4 -0
  19. data/galaxy.gemspec +95 -0
  20. data/galaxy/.loadpath +5 -0
  21. data/galaxy/.project +17 -0
  22. data/galaxy/.settings/org.eclipse.mylyn.tasks.ui.prefs +4 -0
  23. data/galaxy/.settings/org.eclipse.wst.sse.core.prefs +5 -0
  24. data/galaxy/experiments.rb +26 -0
  25. data/lib/galaxy.rb +8 -0
  26. data/lib/galaxy/models/bombing.rb +64 -0
  27. data/lib/galaxy/models/fleet.rb +62 -0
  28. data/lib/galaxy/models/group.rb +178 -0
  29. data/lib/galaxy/models/models.rb +16 -0
  30. data/lib/galaxy/models/planet.rb +181 -0
  31. data/lib/galaxy/models/product.rb +84 -0
  32. data/lib/galaxy/models/race.rb +112 -0
  33. data/lib/galaxy/models/route.rb +60 -0
  34. data/lib/galaxy/order.rb +24 -0
  35. data/lib/galaxy/report.rb +176 -0
  36. data/lib/galaxy/section.rb +226 -0
  37. data/lib/galaxy/utils.rb +109 -0
  38. data/lib/galaxy/virtual_base.rb +165 -0
  39. data/spec/spec_helper.rb +9 -0
  40. data/test/test_helper.rb +4 -0
  41. data/test/unit/models_test.rb +1469 -0
  42. data/test/unit/report_test.rb +187 -0
  43. data/test/unit/utils_test.rb +421 -0
  44. data/test/unit/virtual_base_test.rb +224 -0
  45. metadata +123 -0
@@ -0,0 +1,112 @@
1
+ require 'galaxy/models/models'
2
+
3
+ class Race < ActiveRecord::Base
4
+ #:string, :text, :integer, :float, :decimal, :datetime, :timestamp, :time, :date, :binary, :boolean.
5
+ virtual
6
+ tableless :columns => [
7
+ [ :name, :string ],
8
+ [ :drive, :float ],
9
+ [ :weapons, :float ],
10
+ [ :shields, :float ],
11
+ [ :cargo, :float ],
12
+ [ :pop, :float ],
13
+ [ :ind, :float ],
14
+ [ :num_planets, :integer ],
15
+ [ :relation, :string ],
16
+ [ :vote, :float ]
17
+ ]
18
+ has_many_linked :products
19
+ has_many_linked :planets
20
+ has_many_linked :fleets
21
+ has_many_linked :routes
22
+ has_many_linked :groups
23
+ has_many_linked :bombings
24
+ has_many_linked :incoming_bombings, :class_name => 'Bombing', :foreign_key => 'victim_id'
25
+
26
+ attr_accessor :order
27
+
28
+ def kill
29
+ products.each {|m| m.kill}
30
+ planets.each {|m| m.kill}
31
+ fleets.each {|m| m.kill}
32
+ routes.each {|m| m.kill}
33
+ groups.each {|m| m.kill}
34
+ bombings.each {|m| m.kill}
35
+ incoming_bombings.each {|m| m.kill}
36
+ super #puts "Warning: kill attempt failed for #{self}" unless
37
+ end
38
+
39
+ def initialize match, state
40
+ case match.size
41
+ when 10 then
42
+ super({:name=>match[0], :drive=>match[1].to_f, :weapons=>match[2].to_f, :shields=>match[3].to_f, :cargo=>match[4].to_f,
43
+ :pop=>match[5], :ind=>match[6].to_f, :num_planets=>match[7].to_i, :relation=>match[8], :vote=>match[9].to_f})
44
+ when 0 then # All init data must be given in a state hash
45
+ super state
46
+ end
47
+ add if self.class.dataset # Add instantiated model to dataset if it is defined
48
+ @order = Order.new self, 'zelikaka', state[:game], state[:turn] if your?
49
+ end
50
+
51
+ def key; name end
52
+
53
+ # Special collection accessors
54
+ def sciences; products.find_all {|p| p and p.science?} || [] end # TODO redefine find_all in HashArray?
55
+ def ships; products.find_all {|p| p and p.ship?} || [] end
56
+ alias designs ships
57
+ alias ship_types ships
58
+
59
+
60
+ def battle_groups ; groups.find_all {|g| g and g.from_battle?} || [] end
61
+
62
+ # Boolean tests on Races
63
+ def rip? ; name.split("_")[-1] == 'RIP' end
64
+
65
+ def enemy? ; relation == 'War' end
66
+ alias war? enemy?
67
+
68
+ def friend? ; not enemy? end
69
+ alias ally? friend?
70
+ alias peace? friend?
71
+
72
+ def your? ; relation == '-' end # Redefining TODO or test if this is your other controlled Race (aka 3ombies)
73
+
74
+ def <=>(other)
75
+ case other
76
+ when nil then 1
77
+ when Race then num_planets == other.num_planets ? key <=> other.key : num_planets <=> other.num_planets
78
+ when Product, Planet, Group, Bombing, Route, Fleet then - other <=> self
79
+ when Integer then num_planets <=> other
80
+ when String then self <=> other.downcase.to_sym
81
+ when Symbol then
82
+ return rip? ? 0 : 1 if other == :rip or other == :dead
83
+ return !rip? ? 0 : 1if other == :alive or other == :active
84
+ return enemy? ? 0 :1 if other == :enemy or other == :war
85
+ return friend? ? 0 : 1 if other == :friend or other == :ally or other == :peace
86
+ return your? ? 0 : 1 if other == :your or other == :yours or other == :controlled or other == :my or other == :mine
87
+ return 0 if name.downcase.include? other.to_s
88
+ key <=> other.to_s
89
+ else raise ArgumentError, 'Comparison with a wrong type'
90
+ end
91
+ end
92
+ end
93
+
94
+ class Order
95
+ attr_accessor :race, :text
96
+
97
+ def initialize( race, password, game, turn )
98
+ @race = race
99
+ @password = password
100
+ @game = game
101
+ @turn = turn
102
+ @text = "#order #@game #{@race.name}_#@password turn #@turn\n#end\n"
103
+ end
104
+
105
+ def add_line line
106
+ @text[-6] = "\n" + line + "\n"
107
+ end
108
+
109
+ def recalc_order
110
+ puts "Unable to recalculate: method under construction!"
111
+ end
112
+ end
@@ -0,0 +1,60 @@
1
+ require 'galaxy/models/models'
2
+
3
+ class Route < ActiveRecord::Base
4
+ virtual
5
+ tableless :columns => [
6
+ [ :cargo, :string ]
7
+ ]
8
+ belongs_to :race
9
+ belongs_to :planet
10
+ belongs_to :target, :class_name => "Planet"
11
+
12
+ def initialize match, state # match is not really used, remains for consistency with other models
13
+
14
+ return if match.join == 'N$MCE' # Skip header
15
+ match[1..4].each_with_index do |m,i|
16
+ if m != '-'
17
+ super :cargo => ['cap', 'mat', 'col', 'empty'][i]
18
+ self.target = Planet.new_or_update [m], state.merge({:race=>nil,:product=>nil,:created_by=>self}) unless self.target = Planet.lookup(m)
19
+ end
20
+ end
21
+ planet = Planet.new_or_update [match[0]], state.merge({:race=>nil,:product=>nil,:created_by=>self}) unless planet = Planet.lookup(match[0])
22
+ race = Race.lookup(state[:owner])
23
+
24
+ race.routes << self
25
+ planet.routes << self
26
+ target.incoming_routes << self
27
+ add if self.class.dataset # Add instantiated model to dataset if it is defined
28
+ end
29
+
30
+ def kill
31
+ if result = super
32
+ race.routes.delete self if race
33
+ planet.routes.delete self if planet and planet == self
34
+ target.incoming_routes.delete self if target and target == self
35
+ self.planet = nil
36
+ self.target = nil
37
+ end
38
+ result
39
+ end
40
+
41
+ def key ; [planet.num, target.num, cargo].join('.') end
42
+
43
+ def <=>(other)
44
+ case other
45
+ when nil then 1
46
+ when Route then key <=> other.key
47
+ when Race then race <=> other
48
+ when Planet then planet == other ? 0 : target <=> other
49
+ when Integer, Float then planet.distance(target) <=> other
50
+ when String then self <=> other.downcase.to_sym
51
+ when Symbol then
52
+ return 0 if race == other
53
+ return 0 if planet == other
54
+ return 0 if target == other
55
+ return 0 if cargo.downcase.include? other.to_s
56
+ key.downcase <=> other.to_s
57
+ else raise ArgumentError, 'Comparison with a wrong type'
58
+ end
59
+ end
60
+ end #Route
@@ -0,0 +1,24 @@
1
+ # A <race> - declare peace
2
+ # B <Num> [amount] - break group, also for fleets
3
+ # D <Name><drive><ammo><weap><defence><crg> - design a ship
4
+ # D <Name> - delete a ship
5
+ # G <Num><Race>[amount] - gift a group
6
+ # H <Name><D><W><S><C> - design a science
7
+ # H <Name> - delete a science
8
+ # J <Num><Fleet>[amount] - join group Num to Fleet
9
+ # J <Fleet1><Fleet2> - join F1 to F2
10
+ # K <Num><amount> - take group to pieces
11
+ # L <Num><CargoType>[ShipAmount,[CargoAmount]] - load group
12
+ # N <Planet><NewName> - rename a planet
13
+ # O - options
14
+ # P <Planet><Prod> - set production type
15
+ # Q <GameName><Race><Pass> - quit after 3 turns
16
+ # R <Src><CargoType>[Dst] - route
17
+ # S <Num><Dst>[Amount] - send a group
18
+ # S <Fleet><Dst> - send a fleet
19
+ # T <TypeName><NewName> - rename type
20
+ # U <Num>[ShipAmount[CargoAmount]] - unload group
21
+ # V <Race> - vote
22
+ # W <Race> - declare war
23
+ # X <Num><Tech>[ShipAmount[MaxTech]] - upgrade group
24
+ # Z [parameters] - order a report
@@ -0,0 +1,176 @@
1
+ #TAG report.rb tasks are on
2
+ require 'galaxy/section.rb'
3
+
4
+ # Module Gamedata provides G+ game-related data elements and collections
5
+ # and defines basic accessors to them for any Class including this module
6
+ module Gamedata
7
+ require 'galaxy/utils.rb'
8
+ require 'galaxy/models/models.rb'
9
+
10
+ attr_accessor :order # Order (Object?) associated with this report
11
+ attr_accessor :owner # Report owner race
12
+ attr_accessor :game # Report game name
13
+ attr_accessor :turn # Report turn number
14
+ attr_accessor :time # Report time (from server)
15
+ attr_accessor :server # Report server version (string)
16
+
17
+ # Establish Data Collections
18
+ attr_accessor :races # Races in Report
19
+ attr_accessor :products # Sciences in Report
20
+ attr_accessor :bombings # Bombings in Report
21
+ attr_accessor :planets # Planets in Report
22
+ attr_accessor :routes # Routes in Report
23
+ attr_accessor :fleets # Fleets in Report
24
+ attr_accessor :groups # Groups in Report
25
+ attr_accessor :battles #FIXME Add Battles (based on Battle Protocols) later
26
+
27
+ def initialize *args
28
+ # When object of any including Class is instantiated (and calling super),
29
+ # modify ActiveRecord:Base and point @@dataset attribute to this object
30
+ ActiveRecord::Base.establish_dataset(self)
31
+
32
+ # Initialize G+ collections
33
+ @races = HashArray.new
34
+ @bombings = HashArray.new
35
+ @planets = HashArray.new
36
+ @routes = HashArray.new
37
+ @fleets = HashArray.new
38
+ @groups = HashArray.new
39
+ @products = HashArray.new
40
+
41
+ # Generate Products that are present in each G+ report by default
42
+ @products['Drive_Research'] = Product.new [], {:name=>'Drive', :prod_type=>'research'}
43
+ @products['Weapons_Research'] = Product.new [], {:name=>'Weapons', :prod_type=>'research'}
44
+ @products['Shields_Research'] = Product.new [], {:name=>'Shields', :prod_type=>'research'}
45
+ @products['Cargo_Research'] = Product.new [], {:name=>'Cargo', :prod_type=>'research'}
46
+ @products['Capital'] = Product.new [], {:name=>'Capital', :prod_type=>'cap'}
47
+ @products['Materials'] = Product.new [], {:name=>'Materials', :prod_type=>'mat'}
48
+
49
+ super *args
50
+ end
51
+
52
+ def sciences
53
+ @products.find_all {|p| p and p.race and p.science?} || [] #redefine find_all in HashArray?
54
+ end
55
+
56
+ def ships
57
+ @products.find_all {|p| p and p.race and p.ship?} || [] #redefine find_all in HashArray?
58
+ end
59
+ alias designs ships
60
+
61
+ def battle_groups
62
+ @groups.find_all {|g| g and g.from_battle?} || []
63
+ end
64
+
65
+ def incoming_groups
66
+ @groups.find_all {|g| g and g.incoming?} || []
67
+ end
68
+
69
+ def your_groups
70
+ @groups.find_all {|g| g and g.your?} || []
71
+ end
72
+
73
+ def your_active_groups
74
+ @groups.find_all {|g| g and g.your_active?} || []
75
+ end
76
+
77
+ def unidentified_groups
78
+ @groups.find_all {|g| g and g.unidentified?} || []
79
+ end
80
+ alias unknown_groups unidentified_groups
81
+
82
+ def your_planets
83
+ @planets.find_all {|p| p and p.your?} || []
84
+ end
85
+
86
+ def uninhabited_planets
87
+ @planets.find_all {|p| p and p.uninhabited? } || []
88
+ end
89
+
90
+ def unidentified_planets
91
+ @planets.find_all {|p| p and p.unidentified? } || []
92
+ end
93
+ alias unknown_planets unidentified_planets
94
+ end
95
+
96
+ # Describes Galaxy Plus Report (as received from server) data structures and provides
97
+ # 'parse' method for extracting data it.
98
+ # Initially, Report "contains" extracted data (through included module Gamedata),
99
+ # but later on these data containers should be moved to new Game class
100
+ class Report < Section
101
+ include Gamedata
102
+
103
+ # Describes G+ Report data structure, opens report file if given a valid file name
104
+ def initialize (*args)
105
+
106
+ # Define Proc for Report Header processing TODO
107
+ report_proc = lambda do |match, state|
108
+ @owner = state[:owner] = match[1]
109
+ @game = state[:game] = match[2]
110
+ @turn = state[:turn] = match[3].to_i
111
+ @time = state[:time] = match[4]
112
+ @server = state[:server] = match[5]
113
+ end
114
+
115
+ # Define G+ Report Sections
116
+ @sections = [
117
+ :races,
118
+ {:name=>:science_products, :mult=>true},
119
+ {:name=>:ship_products, :mult=>true},
120
+ {:name=>:battle_planets, :footer => 'Battle Protocol',
121
+ :sections => [Section.new(:name=>:battle_groups, :mult=>true)], :mult=>true },
122
+ :bombings,
123
+ {:header => 'Maps_header', :skip=>true},
124
+ :incoming_groups,
125
+ :your_planets,
126
+ :production_planets,
127
+ :routes,
128
+ {:name=>:planets, :mult=>true},
129
+ :uninhabited_planets,
130
+ :unidentified_planets,
131
+ :fleets,
132
+ :your_groups,
133
+ {:name=>:groups, :mult=>true},
134
+ :unidentified_groups
135
+ ].map do |init| Section.new init end
136
+
137
+ # Initialize main Section and data Collections (from module Gamedata)
138
+ super :name=>:reports, :header_proc=>report_proc, :sections => @sections
139
+
140
+ # Checking arguments
141
+ case args.size
142
+ when 0 then # Do nothing
143
+ when 1 then open *args
144
+ else # Wrong number of arguments, initializer failed
145
+ puts "Usage: Report.new or Report.new(file_name)"
146
+ raise ArgumentError, "Wrong number of arguments in Report initialization"
147
+ end
148
+ end
149
+
150
+ # Validates file name and reads everything from file into Report's @text property
151
+ def open file_name
152
+ if file_name =~ /\A[[:punct:]\w\d]+.rep\z/ and File.exists? file_name
153
+ # This is a valid and existing rep file name
154
+ elsif file_name += '.rep' and file_name =~ /\A[[:punct:]\w\d]+.rep\z/ and File.exists? file_name
155
+ # This is a valid and existing rep file (without rep suffix)
156
+ else
157
+ raise ArgumentError, "Can't open: Invalid Report file #{file_name}"
158
+ end
159
+ # puts "Initializing Report from rep file: " + file_name
160
+ # Open file and pass file stream into block to read from it into Report's @text property
161
+ # File closing is automatic upon execution of the block
162
+ File.open(file_name, "r") do |f| @text = f.read end
163
+ end
164
+
165
+ # Method returns status string of the Report
166
+ def status
167
+ return "
168
+ Report: #@owner #@game #@turn #@time #@server
169
+ Races: #{@races.size} Sciences: #{sciences.size} Types: #{designs.size} BattleGroups: #{battle_groups.size} \
170
+ Bombings: #{@bombings.size} Incomings: #{incoming_groups.size} Your Planets: #{your_planets.size} \
171
+ Ships in Production: #{@productions} Routes: #{@routes.size}
172
+ Planets: #{@planets.size} Uninhabited Planets: #{uninhabited_planets.size} Unidentified Planets: #{unidentified_planets.size} \
173
+ Fleets: #{@fleets.size} Your Groups: #{your_groups.size} Groups: #{@groups.size} Unidentified Groups: #{unidentified_groups.size}"
174
+ end
175
+
176
+ end #class Report
@@ -0,0 +1,226 @@
1
+ #TAG section.rb tasks are on
2
+ #require 'oniguruma'
3
+
4
+ # Regexen module provides pattern Constants that are used by Sections to describe its data structure
5
+ module Regexen
6
+ #Defining basic regex patterns...
7
+ Name = '([[:punct:]\w]+)'
8
+ Sname = ' +?' + Name
9
+ Fname = '^ *?' + Name #first Name in line (may be preceded by spaces, or not)
10
+ Num = '(\d+(?:\.\d+)?)(?=\s)'
11
+ Snum = ' +?' + Num
12
+ Fnum = '^ *?' + Num
13
+ Int = '(\d+)'
14
+ Sint = ' +?' + Int
15
+ Fint = '^ *?' + Int #first int in line (may be preceded by spaces, or not)
16
+ Line = '([[:punct:]\w ]+)'
17
+ Scargo = ' +?(COL|CAP|MAT|-)'
18
+ Sstatus = ' +?(War|Peace|-|In_Battle|Out_Battle|In_Space|In_Orbit|Upgrade|Launched|Transfer_Status|Damaged|Wiped)' #may need further refining for productivity
19
+ Group = Sname + Snum * 4 + Scargo + Snum #Different from actual Groups?! Have to check...
20
+ Header = '\n\n\s*?([PIRVNDAWSCM#TQLOXY$EGF]\s+?)'
21
+
22
+ #Defining section header/footer patterns
23
+ Reports_header = Name + ' Report for Galaxy PLUS' + Sname + ' Turn' + Sint + ' ' + Line + '$\s*' + Line
24
+ #+ '$$\s*Size:' + Snum + '\s*Planets:' + Snum + '\s*Players:'+ Snum
25
+ Races_header = '^\s*?Status of Players .+?' + Snum + ' votes\)' + Header
26
+ Science_products_header = '^\s*?' + Name + ' Sciences' + Header
27
+ Ship_products_header = '^\s*?' + Name + ' Ship Types' + Header
28
+ Battle_planets_header = '^\s*?Battle at \(#' + Int + '\) ' + Name
29
+ Bombings_header = '^\s*?Bombings' + Header
30
+ Maps_header = '^\s*Map around'
31
+ Incoming_groups_header = '^\s*?(Incoming) Groups' + Header
32
+ Your_planets_header = '^\s*?(Your) Planets' + Header
33
+ Planets_header = '^\s*?' + Name + ' Planets' + Header
34
+ Production_planets_header = '^\s*?Ships In Production' + Header
35
+ Routes_header = '^\s*?(Your) Routes' + Header
36
+ Uninhabited_planets_header = '^\s*?(Uninhabited) Planets' + Header
37
+ Unidentified_planets_header = '^\s*?(Unidentified) Planets' + Header
38
+ Fleets_header = '^\s*?(Your) Fleets' + Header
39
+ Your_groups_header = '^\s*?(Your) Groups' + Header
40
+ Groups_header = '^\s*?' + Name + ' Groups(?<!Your Groups)(?<!Unidentified Groups)\s+?' #' Groups(?<!Your Groups)(?<!Unidentified Groups)\s+?' + Header
41
+ Battle_groups_header = Groups_header
42
+ Unidentified_groups_header = '^\s*(Unidentified) Groups' + Header
43
+ Default_footer ='(\n\n)|(\z)'
44
+
45
+ #Defining data record patterns
46
+ Races_record = Fname + Snum * 7 + Sstatus + Snum
47
+ Science_products_record = Fname + Snum * 4
48
+ Ship_products_record = Fname + Snum * 6
49
+ #Battles_line = '^' + Line + '(fires on)' + Line + '($)'
50
+ Battle_planets_record = Battle_planets_header
51
+ Battle_groups_record = Fint + Group + Sint + Sstatus
52
+ Bombings_record = Fname + Sname + Sint + Sname + Snum * 2 + Sname + Snum * 4 + Sstatus
53
+ Incoming_groups_record = Fname + Sname + Snum * 3
54
+ Unidentified_planets_record = Fint + Snum * 2
55
+ Uninhabited_planets_record = Unidentified_planets_record + Sname + Snum * 4
56
+ Planets_record = Uninhabited_planets_record + Sname + Snum * 4
57
+ Your_planets_record = Planets_record
58
+ Production_planets_record = Fint + Sname * 2 + Snum * 3
59
+ Routes_record = Fname + Sname * 4 #Attention! routes_record captures column headers line
60
+ Fleets_record = Fint + Sname + Sint + Sname * 2 + Snum * 2 + Sstatus
61
+ Your_groups_record = Fint + Sint + Group + Sname * 2 + Snum * 3 + Sname + Sstatus
62
+ Groups_record = Fint + Group + Sname + Snum * 2
63
+ Unidentified_groups_record = Fnum + Snum
64
+ # FIXME Broadcasts not defined
65
+
66
+ #Defining data processing Procs
67
+ Default_header_proc = lambda do |match, state|
68
+ state[:race] = match[1] == 'Your' ? Race.lookup(state[:owner]) : Race.lookup(match[1]) if match[1]
69
+ end
70
+ end
71
+
72
+ # Section is a piece of text that contains data structured in a certain way (possibly with sub-sections),
73
+ # Section describes this data structure and provides methods for parsing the text and extracting data records
74
+ # By default, Section calls new_or_update method on class defined by Section name
75
+ # Example: Section(:name=>:battle_groups) calls Group.new_or_update(match, state) on each found /Battle_group_record/ match
76
+ # by default, unless :record_proc is provided to Section
77
+ # 'state' hash is used to provide context for multi-section parsing
78
+ class Section
79
+ include Regexen
80
+
81
+ attr_accessor :name # Name of this Section (also used to auto-generate properties)
82
+ attr_accessor :text # Source text of this Section (raw material for data extraction)
83
+ attr_accessor :header # Regex identifying start of this Section (obligatory!)
84
+ attr_accessor :footer # Regex identifying end of this Section (end of EACH Multisections)
85
+ attr_accessor :record # Regex matching data Record (or an array of such regexen)
86
+ attr_accessor :header_proc # Proc object to be run on Header match
87
+ attr_accessor :footer_proc # Proc object to be run on Footer match
88
+ attr_accessor :record_proc # Proc object to be run on each Record match (or an array of Procs)
89
+ attr_accessor :sections # (Sub)sections (possibly) contained inside this Section
90
+ attr_accessor :skip # Flag indicating that this Sections contains no data (should be skipped)
91
+ attr_accessor :mult # Flag indicating that this is a "Multisection" (several Sections with similar headers one after another)
92
+
93
+ # New Section is created by using the following syntax:
94
+ # Section.new {:header => header, :footer => footer, :record =>[rec1,rec2], :sections => [sec1,sec2,sec3]}
95
+ # Section.new {:name => name} -> extracted as {:header => Name_header, :footer => Name_footer, :record =>Name_record }
96
+ # Section.new :symbol -> extracted as {:header => Symbol_header, :footer => Symbol_footer, :record =>Symbol_record }
97
+ def initialize args
98
+ case args # Parsing (named) arguments
99
+ when Symbol, String # Symbol represents Section name, appropriately named Constants MUST be defined in Regexen module
100
+ @name = args.to_s.downcase.capitalize
101
+ when Hash
102
+ @name = args[:name].to_s.downcase.capitalize if args[:name]
103
+ @text = args[:text]
104
+ @skip = args[:skip]
105
+ @mult = args[:mult]
106
+ @sections = args[:sections]
107
+
108
+ # Header/footer/record patterns and appropriate processing Procs can be:
109
+ # 1) given as a Constant name (should be defined in Regexen module),
110
+ # 2) given as a direct value (escaped pattern string literal or Proc, respectively), or
111
+ # 3) not given at all, appropriate values should be inferred from :name argument
112
+ @header = Regexen.const_get(args[:header]) rescue args[:header]
113
+ @footer = Regexen.const_get(args[:footer]) rescue args[:footer]
114
+ @record = Regexen.const_get(args[:record]) rescue args[:record]
115
+ @header_proc = Regexen.const_get(args[:header_proc]) rescue args[:header_proc]
116
+ @footer_proc = Regexen.const_get(args[:footer_proc]) rescue args[:footer_proc]
117
+ @record_proc = Regexen.const_get(args[:record_proc]) rescue args[:record_proc]
118
+ end #case
119
+
120
+ # Try to auto-generate Section's Patterns and Procs from @name (if they are not already given)
121
+ # First we try to find Regexen constants derived from name, if not found then we look for defaults
122
+ @header = @header || Regexen.const_get(@name + '_header') rescue
123
+ if Regexen.const_defined?('Default_header') then Regexen.const_get('Default_header') end
124
+ @footer = @footer || Regexen.const_get(@name + '_footer') rescue
125
+ if Regexen.const_defined?('Default_footer') then Regexen.const_get('Default_footer') end
126
+ @record = @record || Regexen.const_get(@name + '_record') rescue
127
+ if Regexen.const_defined?('Default_record') then Regexen.const_get('Default_record') end
128
+ @header_proc = @header_proc || Regexen.const_get(@name + '_header_proc') rescue
129
+ if Regexen.const_defined?('Default_header_proc') then Regexen.const_get('Default_header_proc') end
130
+ @footer_proc = @footer_proc || Regexen.const_get(@name + '_footer_proc') rescue
131
+ if Regexen.const_defined?('Default_footer_proc') then Regexen.const_get('Default_footer_proc') end
132
+ @record_proc = @record_proc || Regexen.const_get(@name + '_record_proc') rescue
133
+ if Regexen.const_defined?('Default_record_proc') then Regexen.const_get('Default_record_proc') end
134
+
135
+ # This is a G+ specific piece of code overriding general Section functionality (Default_record_proc)
136
+ # Needed to speed up calculations and avoid class evaluations on each record
137
+ # Class name of the Object (described by Record), e.g "Group"
138
+ if @name and not @record_proc
139
+ klass_name = @name.split("_")[-1][0..-2].capitalize
140
+ if Object.const_defined?(klass_name)
141
+ klass = Object.const_get(klass_name)
142
+ @record_proc ||= lambda do |match, state|
143
+ klass.new_or_update match[1..-1], state
144
+ end
145
+ end
146
+ end
147
+ end #initialize
148
+
149
+ # Returns (relatively) deep copy of self
150
+ def copy
151
+ secs = @sections ? @sections.map {|s| s.copy} : nil
152
+ Section.new :name=>@name, :header=>@header, :footer=>@footer, :record=>@record, :header_proc=>@header_proc,
153
+ :footer_proc=>@footer_proc, :record_proc=>@record_proc, :sections=>secs, :skip=>@skip, :mult=>@mult, :text=>@text
154
+ end
155
+
156
+ # Recursively parse Section, extract data records
157
+ def parse state={}
158
+ state[:section] = @name
159
+ if @mult
160
+ #puts "Mults: #{self.name} #{self.header}"
161
+ # Multisection: Find out if this Section is actually a collection of sections with similar headers
162
+ # If it is, clone an Array of multisections and call parse on each (data extraction happens downstream)
163
+ scan_text(@header) do |match|
164
+ start = match.begin
165
+ finish = -1 unless finish = find_text(@footer, match.end) # Find end of Section (after Header END)
166
+ s = self.copy # Create a copy of Section (to be used as child multisection template)
167
+ s.mult = false
168
+ s.text = @text[start..finish] # Set text property for found multisection
169
+ s.parse state # Recursively call parse on each found multisection
170
+ end
171
+ else
172
+ # Process Section Header, Records and Footer (if any)
173
+ find_text(@header) {|match| @header_proc.call match, state} if @header and @header_proc
174
+ scan_text(@record) {|match| @record_proc.call match, state} if @record and @record_proc
175
+ find_text(@footer) {|match| @footer_proc.call match, state} if @footer and @footer_proc
176
+
177
+ if @sections
178
+ #puts "Sections: #{self.name} #{self.header}"
179
+ # Process Sections array against @text, skipping empty/skippable Sections, recursively
180
+ # calling parse on found Sections and moving forward position cursor pos
181
+ # TODO Generalize for UNORDERED Sections (position cursor should not work in this case)
182
+ finish = 0
183
+ @sections.each_with_index do |s, i|
184
+ next if s.skip #Skip non-data Section
185
+ if start = find_text(s.header, finish) # Find Section Header
186
+ finish = nil # Needed for last Section (no next section to find)
187
+ @sections[i+1..-1].each do |sn| # Find finish by cycling through next Section Headers
188
+ break if finish = find_text(sn.header, start) # Find first of next Section Header
189
+ end
190
+ finish = -1 unless finish # If finish not found, set it to the end of @text
191
+ #Start and finish defined, assign text to this Section and recursively parse it
192
+ s.text = @text[start..finish]
193
+ s.parse state
194
+ end
195
+ end
196
+ end
197
+ end
198
+ end #parse
199
+
200
+ # Safely matches given regex to @text (starting at position pos),
201
+ # returns initial offset of match or nil if regex not found, yield match to given block (if any)
202
+ def find_text regex, pos=0
203
+ return nil if @text == nil
204
+ return nil if regex == nil
205
+ text = pos == 0 ? @text : @text[pos..-1]
206
+ match = Oniguruma::ORegexp.new(regex).match(text)
207
+ return nil unless match
208
+ yield match if block_given?
209
+ pos + match.begin # Return initial match offset (corrected for position pos)
210
+ end #find_text
211
+
212
+ # Scans @text for Data Records matching given regex pattern, returns array of matching Data Records
213
+ # (as MatchData or String array), yields each found match object to given block (if any)
214
+ def scan_text regex, pos=0
215
+ text = pos == 0 ? @text : @text[pos..-1]
216
+ if block_given?
217
+ # Scan Section for regex matches, yield each match to given block, return array of MATCH objects
218
+ Oniguruma::ORegexp.new(regex).scan(text) {|match| yield match }
219
+ else
220
+ # Scan Section for regex matches, return array of matches converted into string arrays
221
+ results=[]
222
+ Oniguruma::ORegexp.new(regex).scan(text) {|match| results << match[1..-1].to_a }
223
+ results
224
+ end
225
+ end #scan_text
226
+ end #class Section