depq 0.1 → 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 (4) hide show
  1. data/README +6 -9
  2. data/depq.rb +363 -60
  3. data/test-depq.rb +46 -0
  4. metadata +4 -4
data/README CHANGED
@@ -139,15 +139,12 @@ A* search algorithm, etc.
139
139
  # [["A", "B", "C"], 3],
140
140
  # [["A", "B", "C", "D"], 4]]
141
141
 
142
- = Internal Heap Algorithm and Performance Tips
143
-
144
- Depq uses min-heap or max-heap internally.
145
- When delete_min is used, min-heap is constructed and max-heap is destructed.
146
- When delete_max is used, max-heap is constructed and min-heap is destructed.
147
- So mixing delete_min and delete_max causes bad performance.
148
- In future, min-max-heap may be implemented to avoid this problem.
149
- min-max-heap will be used when delete_min and delete_max is used both.
150
- (Because min-max-heap is slower than min-heap/max-heap.)
142
+ = Internal Heap Algorithm
143
+
144
+ Depq uses min-heap, max-heap or interval-heap internally.
145
+ When delete_min is used, min-heap is constructed.
146
+ When delete_max is used, max-heap is constructed.
147
+ When delete_min and delete_max is used, interval-heap is constructed.
151
148
 
152
149
  = Author
153
150
 
data/depq.rb CHANGED
@@ -165,15 +165,12 @@
165
165
  # # [["A", "B", "C"], 3],
166
166
  # # [["A", "B", "C", "D"], 4]]
167
167
  #
168
- # = Internal Heap Algorithm and Performance Tips
168
+ # = Internal Heap Algorithm
169
169
  #
170
- # Depq uses min-heap or max-heap internally.
171
- # When delete_min is used, min-heap is constructed and max-heap is destructed.
172
- # When delete_max is used, max-heap is constructed and min-heap is destructed.
173
- # So mixing delete_min and delete_max causes bad performance.
174
- # In future, min-max-heap may be implemented to avoid this problem.
175
- # min-max-heap will be used when delete_min and delete_max is used both.
176
- # (Because min-max-heap is slower than min-heap/max-heap.)
170
+ # Depq uses min-heap, max-heap or interval-heap internally.
171
+ # When delete_min is used, min-heap is constructed.
172
+ # When delete_max is used, max-heap is constructed.
173
+ # When delete_min and delete_max is used, interval-heap is constructed.
177
174
  #
178
175
  class Depq
179
176
  include Enumerable
@@ -217,8 +214,6 @@ class Depq
217
214
  define_method(:eql?, Object.instance_method(:eql?))
218
215
  define_method(:hash, Object.instance_method(:hash))
219
216
 
220
- include Comparable
221
-
222
217
  # Create a Depq::Locator object.
223
218
  def initialize(value, priority=value, subpriority=nil)
224
219
  super value, subpriority, priority
@@ -447,25 +442,41 @@ class Depq
447
442
  end
448
443
  private :each_entry
449
444
 
450
- def min_mode
451
- if @mode != MinHeap
445
+ def use_min
446
+ if @mode == MinHeap || @mode == IntervalHeap
447
+ if @heapsize < self.size
448
+ @heapsize = @mode.heapify(self, @ary, @heapsize)
449
+ end
450
+ else
452
451
  @mode = MinHeap
453
452
  @heapsize = @mode.heapify(self, @ary)
454
- elsif @heapsize < self.size
455
- @heapsize = @mode.heapify(self, @ary, @heapsize)
456
453
  end
457
454
  end
458
- private :min_mode
455
+ private :use_min
459
456
 
460
- def max_mode
461
- if @mode != MaxHeap
457
+ def use_max
458
+ if @mode == MaxHeap || @mode == IntervalHeap
459
+ if @heapsize < self.size
460
+ @heapsize = @mode.heapify(self, @ary, @heapsize)
461
+ end
462
+ else
462
463
  @mode = MaxHeap
463
464
  @heapsize = @mode.heapify(self, @ary)
464
- elsif @heapsize < self.size
465
- @heapsize = @mode.heapify(self, @ary, @heapsize)
466
465
  end
467
466
  end
468
- private :max_mode
467
+ private :use_max
468
+
469
+ def use_minmax
470
+ if @mode == IntervalHeap
471
+ if @heapsize < self.size
472
+ @heapsize = @mode.heapify(self, @ary, @heapsize)
473
+ end
474
+ else
475
+ @mode = IntervalHeap
476
+ @heapsize = @mode.heapify(self, @ary)
477
+ end
478
+ end
479
+ private :use_minmax
469
480
 
470
481
  def mode_heapify
471
482
  if @mode
@@ -756,8 +767,8 @@ class Depq
756
767
  #
757
768
  def find_min_locator
758
769
  return nil if empty?
759
- min_mode
760
- @mode.find_min_locator(@ary)
770
+ use_min
771
+ @mode.find_min_locator(self, @ary)
761
772
  end
762
773
 
763
774
  # return the minimum value with its priority.
@@ -810,8 +821,8 @@ class Depq
810
821
  #
811
822
  def find_max_locator
812
823
  return nil if empty?
813
- max_mode
814
- @mode.find_max_locator(@ary)
824
+ use_max
825
+ @mode.find_max_locator(self, @ary)
815
826
  end
816
827
 
817
828
  # return the maximum value with its priority.
@@ -862,35 +873,8 @@ class Depq
862
873
  #
863
874
  def find_minmax_locator
864
875
  return [nil, nil] if empty?
865
- case @mode
866
- when :min
867
- loc1 = find_min_locator
868
- loc2 = loc1
869
- self.each_locator {|loc|
870
- if compare_for_max(loc2.priority, loc2.subpriority, loc.priority, loc.subpriority) < 0
871
- loc2 = loc
872
- end
873
- }
874
- when :max
875
- loc2 = find_max_locator
876
- loc1 = loc2
877
- self.each_locator {|loc|
878
- if compare_for_min(loc1.priority, loc1.subpriority, loc.priority, loc.subpriority) > 0
879
- loc1 = loc
880
- end
881
- }
882
- else
883
- loc1 = loc2 = nil
884
- self.each_locator {|loc|
885
- if loc1 == nil || compare_for_min(loc1.priority, loc1.subpriority, loc.priority, loc.subpriority) > 0
886
- loc1 = loc
887
- end
888
- if loc2 == nil || compare_for_max(loc2.priority, loc2.subpriority, loc.priority, loc.subpriority) < 0
889
- loc2 = loc
890
- end
891
- }
892
- end
893
- [loc1, loc2]
876
+ use_minmax
877
+ return @mode.find_minmax_locator(self, @ary)
894
878
  end
895
879
 
896
880
  # returns the minimum and maximum value as a two-element array.
@@ -953,8 +937,8 @@ class Depq
953
937
  #
954
938
  def delete_min_locator
955
939
  return nil if empty?
956
- min_mode
957
- loc = @mode.find_min_locator(@ary)
940
+ use_min
941
+ loc = @mode.find_min_locator(self, @ary)
958
942
  @heapsize = @mode.delete_locator(self, @ary, loc)
959
943
  loc
960
944
  end
@@ -1018,8 +1002,8 @@ class Depq
1018
1002
  #
1019
1003
  def delete_max_locator
1020
1004
  return nil if empty?
1021
- max_mode
1022
- loc = @mode.find_max_locator(@ary)
1005
+ use_max
1006
+ loc = @mode.find_max_locator(self, @ary)
1023
1007
  @heapsize = @mode.delete_locator(self, @ary, loc)
1024
1008
  loc
1025
1009
  end
@@ -1320,8 +1304,7 @@ class Depq
1320
1304
 
1321
1305
  # :stopdoc:
1322
1306
 
1323
- module SimpleHeap
1324
-
1307
+ module HeapArray
1325
1308
  def size(ary)
1326
1309
  return ary.size / ARY_SLICE_SIZE
1327
1310
  end
@@ -1364,6 +1347,10 @@ class Depq
1364
1347
  ei.send(:index=, j)
1365
1348
  ej.send(:index=, i)
1366
1349
  end
1350
+ end
1351
+
1352
+ module SimpleHeap
1353
+ include HeapArray
1367
1354
 
1368
1355
  def upheap(pd, ary, j)
1369
1356
  while true
@@ -1393,7 +1380,7 @@ class Depq
1393
1380
  end
1394
1381
  end
1395
1382
 
1396
- def find_top_locator(ary)
1383
+ def find_top_locator(pd, ary)
1397
1384
  loc, _ = get_entry(ary, 0)
1398
1385
  loc
1399
1386
  end
@@ -1516,5 +1503,321 @@ class Depq
1516
1503
  alias find_max_locator find_top_locator
1517
1504
  end
1518
1505
 
1506
+ module IntervalHeap
1507
+ end
1508
+ class << IntervalHeap
1509
+ include HeapArray
1510
+
1511
+ def root?(i) i < 2 end
1512
+ def minside?(i) i.even? end
1513
+ def maxside?(i) i.odd? end
1514
+ def minside(i) i & ~1 end
1515
+ def maxside(i) i | 1 end
1516
+ def parent_minside(j) (j-2)/2 & ~1 end
1517
+ def parent_maxside(j) (j-2)/2 | 1 end
1518
+ def child1_minside(i) i &= ~1; (i*2+2) & ~1 end
1519
+ def child1_maxside(i) i &= ~1; (i*2+2) | 1 end
1520
+ def child2_minside(i) i &= ~1; (i*2+4) & ~1 end
1521
+ def child2_maxside(i) i &= ~1; (i*2+4) | 1 end
1522
+
1523
+ def pcmp(pd, ary, i, j)
1524
+ ei, pi, si = get_entry(ary, i)
1525
+ ej, pj, sj = get_entry(ary, j)
1526
+ pd.compare_priority(pi, pj)
1527
+ end
1528
+
1529
+ def scmp(pd, ary, i, j)
1530
+ ei, pi, si = get_entry(ary, i)
1531
+ ej, pj, sj = get_entry(ary, j)
1532
+ si <=> sj
1533
+ end
1534
+
1535
+ def psame(pd, ary, i)
1536
+ pcmp(pd, ary, minside(i), maxside(i)) == 0
1537
+ end
1538
+
1539
+ def travel(pd, ary, i, range, fix_subpriority)
1540
+ while true
1541
+ j = yield i
1542
+ return i if !j
1543
+ swap ary, i, j
1544
+ if fix_subpriority
1545
+ imin = minside(i)
1546
+ imax = maxside(i)
1547
+ if range.include?(imin) && range.include?(imax)
1548
+ if pcmp(pd, ary, imin, imax) == 0 && scmp(pd, ary, imin, imax) > 0
1549
+ swap ary, imin, imax
1550
+ end
1551
+ end
1552
+ end
1553
+ i = j
1554
+ end
1555
+ end
1556
+
1557
+ def upheap_minside(pd, ary, i, range)
1558
+ travel(pd, ary, i, range, true) {|j|
1559
+ if root?(j)
1560
+ nil
1561
+ elsif !range.include?(k = parent_minside(j))
1562
+ nil
1563
+ else
1564
+ if pcmp(pd, ary, k, j) > 0
1565
+ swap(ary, minside(k), maxside(k)) if psame(pd, ary, k)
1566
+ k
1567
+ else
1568
+ nil
1569
+ end
1570
+ end
1571
+ }
1572
+ end
1573
+
1574
+ def upheap_maxside(pd, ary, i, range)
1575
+ travel(pd, ary, i, range, true) {|j|
1576
+ if root?(j)
1577
+ nil
1578
+ elsif !range.include?(k = parent_maxside(j))
1579
+ nil
1580
+ else
1581
+ if pcmp(pd, ary, k, j) < 0
1582
+ k
1583
+ else
1584
+ nil
1585
+ end
1586
+ end
1587
+ }
1588
+ end
1589
+
1590
+ def downheap_minside(pd, ary, i, range)
1591
+ travel(pd, ary, i, range, true) {|j|
1592
+ k1 = child1_minside(j)
1593
+ k2 = child2_minside(j)
1594
+ if !range.include?(k1)
1595
+ nil
1596
+ else
1597
+ if !range.include?(k2)
1598
+ k = k1
1599
+ else
1600
+ if (pc = pcmp(pd, ary, k1, k2)) < 0
1601
+ k = k1
1602
+ elsif pc > 0
1603
+ k = k2
1604
+ elsif (sc = scmp(pd, ary, k1, k2)) <= 0
1605
+ k = k1
1606
+ else
1607
+ k = k2
1608
+ end
1609
+ end
1610
+ if (pc = pcmp(pd, ary, k, j)) < 0
1611
+ k
1612
+ else
1613
+ nil
1614
+ end
1615
+ end
1616
+ }
1617
+ end
1618
+
1619
+ def downheap_maxside(pd, ary, i, range)
1620
+ travel(pd, ary, i, range, true) {|j|
1621
+ k1 = child1_maxside(j)
1622
+ k2 = child2_maxside(j)
1623
+ k1 = minside(k1) if range.include?(k1) && psame(pd, ary, k1)
1624
+ k2 = minside(k2) if range.include?(k2) && psame(pd, ary, k2)
1625
+ if !range.include?(k1)
1626
+ nil
1627
+ else
1628
+ if !range.include?(k2)
1629
+ k = k1
1630
+ else
1631
+ if (pc = pcmp(pd, ary, k1, k2)) < 0
1632
+ k = k2
1633
+ elsif pc > 0
1634
+ k = k1
1635
+ elsif (sc = scmp(pd, ary, k1, k2)) <= 0
1636
+ k = k1
1637
+ else
1638
+ k = k2
1639
+ end
1640
+ end
1641
+ if (pc = pcmp(pd, ary, k, j)) > 0
1642
+ swap(ary, minside(k), maxside(k)) if minside?(k)
1643
+ maxside(k)
1644
+ else
1645
+ nil
1646
+ end
1647
+ end
1648
+ }
1649
+ end
1650
+
1651
+ def upheap_sub(pd, ary, i, range)
1652
+ travel(pd, ary, i, range, false) {|j|
1653
+ k = nil
1654
+ if minside?(j)
1655
+ if range.include?(kk=parent_maxside(j)) && pcmp(pd, ary, j, kk) == 0
1656
+ k = kk
1657
+ elsif range.include?(kk=parent_minside(j)) && pcmp(pd, ary, j, kk) == 0
1658
+ k = kk
1659
+ end
1660
+ else
1661
+ if range.include?(kk=minside(j)) && pcmp(pd, ary, j, kk) == 0
1662
+ k = kk
1663
+ elsif range.include?(kk=parent_maxside(j)) && pcmp(pd, ary, j, kk) == 0
1664
+ k = kk
1665
+ end
1666
+ end
1667
+ if !k
1668
+ nil
1669
+ elsif !range.include?(k)
1670
+ nil
1671
+ elsif scmp(pd, ary, k, j) > 0
1672
+ k
1673
+ else
1674
+ nil
1675
+ end
1676
+ }
1677
+ end
1678
+
1679
+ def downheap_sub(pd, ary, i, range)
1680
+ travel(pd, ary, i, range, false) {|j|
1681
+ k1 = k2 = nil
1682
+ if minside?(j)
1683
+ if range.include?(kk=maxside(j)) && pcmp(pd, ary, j, kk) == 0
1684
+ k1 = kk
1685
+ else
1686
+ k1 = kk if range.include?(kk=child1_minside(j)) && pcmp(pd, ary, j, kk) == 0
1687
+ k2 = kk if range.include?(kk=child2_minside(j)) && pcmp(pd, ary, j, kk) == 0
1688
+ end
1689
+ else
1690
+ if range.include?(kk=child1_minside(j)) && pcmp(pd, ary, j, kk) == 0
1691
+ k1 = kk
1692
+ elsif range.include?(kk=child1_maxside(j)) && pcmp(pd, ary, j, kk) == 0
1693
+ k1 = kk
1694
+ end
1695
+ if range.include?(kk=child2_minside(j)) && pcmp(pd, ary, j, kk) == 0
1696
+ k2 = kk
1697
+ elsif range.include?(kk=child2_maxside(j)) && pcmp(pd, ary, j, kk) == 0
1698
+ k2 = kk
1699
+ end
1700
+ end
1701
+ if k1 && k2
1702
+ k = scmp(pd, ary, k1, k2) > 0 ? k2 : k1
1703
+ else
1704
+ k = k1 || k2
1705
+ end
1706
+ if k && scmp(pd, ary, k, j) < 0
1707
+ k
1708
+ else
1709
+ nil
1710
+ end
1711
+ }
1712
+ end
1713
+
1714
+ def adjust(pd, ary, i, range)
1715
+ if minside?(i)
1716
+ j = upheap_minside(pd, ary, i, range)
1717
+ if i == j
1718
+ i = downheap_minside(pd, ary, i, range)
1719
+ if !range.include?(child1_minside(i)) && range.include?(j=maxside(i)) && pcmp(pd, ary, i, j) > 0
1720
+ swap(ary, i, j)
1721
+ i = j
1722
+ end
1723
+ if maxside?(i) || !range.include?(maxside(i))
1724
+ i = upheap_maxside(pd, ary, i, range)
1725
+ end
1726
+ end
1727
+ else
1728
+ j = upheap_maxside(pd, ary, i, range)
1729
+ if i == j
1730
+ i = downheap_maxside(pd, ary, i, range)
1731
+ if !range.include?(child1_maxside(i))
1732
+ if range.include?(j=child1_minside(i)) && pcmp(pd, ary, j, i) > 0
1733
+ swap(ary, i, j)
1734
+ i = j
1735
+ elsif range.include?(j=minside(i)) && pcmp(pd, ary, j, i) > 0
1736
+ swap(ary, i, j)
1737
+ i = j
1738
+ end
1739
+ end
1740
+ if minside?(i)
1741
+ i = upheap_minside(pd, ary, i, range)
1742
+ end
1743
+ end
1744
+ end
1745
+ i = upheap_sub(pd, ary, i, range)
1746
+ downheap_sub(pd, ary, i, range)
1747
+ end
1748
+
1749
+ def update_priority(pd, ary, loc, prio, subprio)
1750
+ i = loc.send(:index)
1751
+ ei, pi, si = get_entry(ary, i)
1752
+ subpriority ||= si
1753
+ set_entry(ary, i, ei, prio, subprio)
1754
+ range = 0...size(ary)
1755
+ adjust(pd, ary, i, range)
1756
+ end
1757
+
1758
+ def insert_internal(pd, ary, loc, prio, subprio)
1759
+ i = size(ary)
1760
+ set_entry(ary, i, loc, prio, subprio)
1761
+ range = 0...size(ary)
1762
+ adjust(pd, ary, i, range)
1763
+ end
1764
+
1765
+ def heapify(pd, ary, heapsize=0)
1766
+ currentsize = size(ary)
1767
+ h = Math.log(currentsize+1)/Math.log(2)
1768
+ if currentsize - 1 < (h - 1) * (currentsize - heapsize + 1)
1769
+ (currentsize-1).downto(0) {|i|
1770
+ adjust(pd, ary, i, i...currentsize)
1771
+ }
1772
+ else
1773
+ heapsize.upto(currentsize-1) {|i|
1774
+ adjust(pd, ary, i, 0...(i+1))
1775
+ }
1776
+ end
1777
+ currentsize
1778
+ end
1779
+
1780
+ def find_minmax_locator(pd, ary)
1781
+ case size(ary)
1782
+ when 0
1783
+ [nil, nil]
1784
+ when 1
1785
+ e0, p0, s0 = get_entry(ary, 0)
1786
+ [e0, e0]
1787
+ else
1788
+ if pcmp(pd, ary, 0, 1) == 0
1789
+ e0, p0, s0 = get_entry(ary, 0)
1790
+ [e0, e0]
1791
+ else
1792
+ e0, p0, s0 = get_entry(ary, 0)
1793
+ e1, p1, s1 = get_entry(ary, 1)
1794
+ [e0, e1]
1795
+ end
1796
+ end
1797
+ end
1798
+
1799
+ def find_min_locator(pd, ary)
1800
+ find_minmax_locator(pd, ary).first
1801
+ end
1802
+
1803
+ def find_max_locator(pd, ary)
1804
+ find_minmax_locator(pd, ary).last
1805
+ end
1806
+
1807
+ def delete_locator(pd, ary, loc)
1808
+ i = loc.send(:index)
1809
+ _, priority, subpriority = get_entry(ary, i)
1810
+ last = size(ary) - 1
1811
+ loc.send(:internal_deleted, priority, subpriority)
1812
+ el, pl, sl = delete_entry(ary, last)
1813
+ if i != last
1814
+ set_entry(ary, i, el, pl, sl)
1815
+ el.send(:index=, i)
1816
+ adjust(pd, ary, i, 0...last)
1817
+ end
1818
+ size(ary)
1819
+ end
1820
+ end
1821
+
1519
1822
  # :startdoc:
1520
1823
  end
data/test-depq.rb CHANGED
@@ -46,6 +46,30 @@ class Depq
46
46
  end
47
47
  end
48
48
 
49
+ def IntervalHeap.validation(pd, ary)
50
+ range=0...size(ary)
51
+ range.each {|j|
52
+ imin = parent_minside(j)
53
+ imax = parent_maxside(j)
54
+ jmin = minside(j)
55
+ if minside?(j) && range.include?(imin) && pcmp(pd, ary, imin, j) > 0
56
+ raise "ary[#{imin}].priority > ary[#{j}].priority "
57
+ end
58
+ if maxside?(j) && range.include?(imax) && pcmp(pd, ary, imax, j) < 0
59
+ raise "ary[#{imax}].priority < ary[#{j}].priority "
60
+ end
61
+ if range.include?(imin) && pcmp(pd, ary, imin, j) == 0 && scmp(pd, ary, imin, j) > 0
62
+ raise "ary[#{imin}].subpriority < ary[#{j}].subpriority "
63
+ end
64
+ if range.include?(imax) && pcmp(pd, ary, imax, j) == 0 && scmp(pd, ary, imax, j) > 0
65
+ raise "ary[#{imax}].subpriority < ary[#{j}].subpriority "
66
+ end
67
+ if maxside?(j) && range.include?(jmin) && pcmp(pd, ary, jmin, j) == 0 && scmp(pd, ary, jmin, j) > 0
68
+ raise "ary[#{jmin}].subpriority < ary[#{j}].subpriority "
69
+ end
70
+ }
71
+ end
72
+
49
73
  def validation
50
74
  @mode.validation(self, @ary) if @mode
51
75
  if @ary.length % ARY_SLICE_SIZE != 0
@@ -580,6 +604,28 @@ class TestDepq < Test::Unit::TestCase
580
604
  assert_equal([1, 3], res)
581
605
  end
582
606
 
607
+ def test_find_minmax_after_min
608
+ pd = Depq.new
609
+ assert_equal([nil, nil], pd.find_minmax)
610
+ pd.insert 3
611
+ pd.insert 1
612
+ pd.insert 2
613
+ assert_equal(1, pd.min)
614
+ res = pd.find_minmax
615
+ assert_equal([1, 3], res)
616
+ end
617
+
618
+ def test_find_minmax_after_max
619
+ pd = Depq.new
620
+ assert_equal([nil, nil], pd.find_minmax)
621
+ pd.insert 3
622
+ pd.insert 1
623
+ pd.insert 2
624
+ assert_equal(3, pd.max)
625
+ res = pd.find_minmax
626
+ assert_equal([1, 3], res)
627
+ end
628
+
583
629
  def test_delete_locator
584
630
  pd = Depq.new
585
631
  loc = pd.insert 1
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: depq
3
3
  version: !ruby/object:Gem::Version
4
- version: "0.1"
4
+ version: "0.2"
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tanaka Akira
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-09-20 00:00:00 +09:00
12
+ date: 2009-09-23 00:00:00 +09:00
13
13
  default_executable:
14
14
  dependencies: []
15
15
 
@@ -30,7 +30,7 @@ files:
30
30
  - README
31
31
  - depq.rb
32
32
  has_rdoc: true
33
- homepage: http://www.a-k-r.org/depq/
33
+ homepage: http://depq.rubyforge.org/
34
34
  licenses: []
35
35
 
36
36
  post_install_message:
@@ -52,7 +52,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
52
52
  version:
53
53
  requirements: []
54
54
 
55
- rubyforge_project:
55
+ rubyforge_project: depq
56
56
  rubygems_version: 1.3.4
57
57
  signing_key:
58
58
  specification_version: 3