galaxy 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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